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

package com.mysql.cj.jdbc;

import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.sql.BatchUpdateException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;

import com.mysql.cj.CancelQueryTask;
import com.mysql.cj.Messages;
import com.mysql.cj.MysqlType;
import com.mysql.cj.NativeSession;
import com.mysql.cj.PingTarget;
import com.mysql.cj.Query;
import com.mysql.cj.QueryAttributesBindings;
import com.mysql.cj.QueryInfo;
import com.mysql.cj.QueryReturnType;
import com.mysql.cj.Session;
import com.mysql.cj.SimpleQuery;
import com.mysql.cj.TransactionEventHandler;
import com.mysql.cj.conf.HostInfo;
import com.mysql.cj.conf.PropertyDefinitions;
import com.mysql.cj.conf.PropertyKey;
import com.mysql.cj.conf.RuntimeProperty;
import com.mysql.cj.exceptions.AssertionFailedException;
import com.mysql.cj.exceptions.CJException;
import com.mysql.cj.exceptions.CJOperationNotSupportedException;
import com.mysql.cj.exceptions.CJTimeoutException;
import com.mysql.cj.exceptions.ExceptionFactory;
import com.mysql.cj.exceptions.ExceptionInterceptor;
import com.mysql.cj.exceptions.MysqlErrorNumbers;
import com.mysql.cj.exceptions.OperationCancelledException;
import com.mysql.cj.exceptions.StatementIsClosedException;
import com.mysql.cj.jdbc.exceptions.MySQLStatementCancelledException;
import com.mysql.cj.jdbc.exceptions.MySQLTimeoutException;
import com.mysql.cj.jdbc.exceptions.SQLError;
import com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping;
import com.mysql.cj.jdbc.result.CachedResultSetMetaData;
import com.mysql.cj.jdbc.result.ResultSetFactory;
import com.mysql.cj.jdbc.result.ResultSetImpl;
import com.mysql.cj.jdbc.result.ResultSetInternalMethods;
import com.mysql.cj.log.ProfilerEvent;
import com.mysql.cj.protocol.Message;
import com.mysql.cj.protocol.ProtocolEntityFactory;
import com.mysql.cj.protocol.Resultset;
import com.mysql.cj.protocol.Resultset.Type;
import com.mysql.cj.protocol.a.NativeConstants;
import com.mysql.cj.protocol.a.NativeMessageBuilder;
import com.mysql.cj.protocol.a.result.ByteArrayRow;
import com.mysql.cj.protocol.a.result.ResultsetRowsStatic;
import com.mysql.cj.result.DefaultColumnDefinition;
import com.mysql.cj.result.Field;
import com.mysql.cj.result.Row;
import com.mysql.cj.telemetry.TelemetryAttribute;
import com.mysql.cj.telemetry.TelemetryScope;
import com.mysql.cj.telemetry.TelemetrySpan;
import com.mysql.cj.telemetry.TelemetrySpanName;
import com.mysql.cj.util.StringUtils;
import com.mysql.cj.util.Util;

/**
 * A Statement object is used for executing a static SQL statement and obtaining the results produced by it.
 *
 * Only one ResultSet per Statement can be open at any point in time. Therefore, if the reading of one ResultSet is interleaved with the reading of another,
 * each must have been generated by different Statements. All statement execute methods implicitly close a statement's current ResultSet if an open one exists.
 */
public class StatementImpl implements JdbcStatement {

    protected static final String PING_MARKER = "/* ping */";

    public final static byte USES_VARIABLES_FALSE = 0;
    public final static byte USES_VARIABLES_TRUE = 1;
    public final static byte USES_VARIABLES_UNKNOWN = -1;

    protected NativeMessageBuilder commandBuilder = null; // TODO use shared builder

    /** The character encoding to use (if available) */
    protected String charEncoding = null;

    /** The connection that created us */
    protected volatile JdbcConnection connection = null;

    /** Should we process escape codes? */
    protected boolean doEscapeProcessing = true;

    /** Has this statement been closed? */
    protected boolean isClosed = false;

    /** The auto_increment value for the last insert */
    protected long lastInsertId = -1;

    /** The max field size for this statement */
    protected int maxFieldSize = (Integer) PropertyDefinitions.getPropertyDefinition(PropertyKey.maxAllowedPacket).getDefaultValue();

    /** The maximum number of rows to return for this statement (-1 means _all_rows) */
    public int maxRows = -1;

    /** Set of currently-open ResultSets */
    private Set<ResultSetInternalMethods> openResultSets = new HashSet<>();

    /** Are we in pedantic mode? */
    protected boolean pedantic = false;

    /** Should we profile? */
    protected boolean profileSQL = false;

    /** The current results */
    protected ResultSetInternalMethods results = null;

    protected ResultSetInternalMethods generatedKeysResults = null;

    /** The concurrency for this result set (updatable or not) */
    protected int resultSetConcurrency = 0;

    /** The update count for this statement */
    protected long updateCount = -1;

    /** Should we use the usage advisor? */
    protected boolean useUsageAdvisor = false;

    /** The warnings chain. */
    protected SQLWarning warningChain = null;

    /** Should this statement hold results open over .close() regardless of connection's setting? */
    protected boolean holdResultsOpenOverClose = false;

    protected ArrayList<Row> batchedGeneratedKeys = null;

    protected boolean retrieveGeneratedKeys = false;

    protected boolean continueBatchOnError = false;

    protected PingTarget pingTarget = null;

    protected ExceptionInterceptor exceptionInterceptor;

    /** Whether or not the last query was of the form ON DUPLICATE KEY UPDATE */
    protected boolean lastQueryIsOnDupKeyUpdate = false;

    protected RuntimeProperty<Boolean> dontTrackOpenResources;
    protected RuntimeProperty<Boolean> dumpQueriesOnException;
    protected boolean logSlowQueries = false;
    protected RuntimeProperty<Boolean> rewriteBatchedStatements;
    protected RuntimeProperty<Integer> maxAllowedPacket;
    protected boolean dontCheckOnDuplicateKeyUpdateInSQL;

    protected ResultSetFactory resultSetFactory;

    protected Query query;
    protected NativeSession session = null;

    /**
     * Constructor for a Statement.
     *
     * @param c
     *            the Connection instance that creates us
     * @param db
     *            the database name in use when we were created
     *
     * @throws SQLException
     *             if an error occurs.
     */
    public StatementImpl(JdbcConnection c, String db) throws SQLException {
        if (c == null || c.isClosed()) {
            throw SQLError.createSQLException(Messages.getString("Statement.0"), MysqlErrorNumbers.SQLSTATE_CONNECTION_EXCEPTION_CONNECTION_DOES_NOT_EXIST,
                    null);
        }

        this.connection = c;
        this.session = (NativeSession) c.getSession();
        this.exceptionInterceptor = c.getExceptionInterceptor();

        this.commandBuilder = new NativeMessageBuilder(this.session.getServerSession().supportsQueryAttributes());

        try {
            initQuery();
        } catch (CJException e) {
            throw SQLExceptionsMapping.translateException(e, getExceptionInterceptor());
        }

        this.query.setCurrentDatabase(db);

        JdbcPropertySet pset = c.getPropertySet();

        this.dontTrackOpenResources = pset.getBooleanProperty(PropertyKey.dontTrackOpenResources);
        this.dumpQueriesOnException = pset.getBooleanProperty(PropertyKey.dumpQueriesOnException);
        this.continueBatchOnError = pset.getBooleanProperty(PropertyKey.continueBatchOnError).getValue();
        this.pedantic = pset.getBooleanProperty(PropertyKey.pedantic).getValue();
        this.rewriteBatchedStatements = pset.getBooleanProperty(PropertyKey.rewriteBatchedStatements);
        this.charEncoding = pset.getStringProperty(PropertyKey.characterEncoding).getValue();
        this.profileSQL = pset.getBooleanProperty(PropertyKey.profileSQL).getValue();
        this.useUsageAdvisor = pset.getBooleanProperty(PropertyKey.useUsageAdvisor).getValue();
        this.logSlowQueries = pset.getBooleanProperty(PropertyKey.logSlowQueries).getValue();
        this.maxAllowedPacket = pset.getIntegerProperty(PropertyKey.maxAllowedPacket);
        this.dontCheckOnDuplicateKeyUpdateInSQL = pset.getBooleanProperty(PropertyKey.dontCheckOnDuplicateKeyUpdateInSQL).getValue();
        this.doEscapeProcessing = pset.getBooleanProperty(PropertyKey.enableEscapeProcessing).getValue();

        this.maxFieldSize = this.maxAllowedPacket.getValue();

        if (!this.dontTrackOpenResources.getValue()) {
            c.registerStatement(this);
        }

        int defaultFetchSize = pset.getIntegerProperty(PropertyKey.defaultFetchSize).getValue();
        if (defaultFetchSize != 0) {
            setFetchSize(defaultFetchSize);
        }

        int maxRowsConn = pset.getIntegerProperty(PropertyKey.maxRows).getValue();

        if (maxRowsConn != -1) {
            setMaxRows(maxRowsConn);
        }

        this.holdResultsOpenOverClose = pset.getBooleanProperty(PropertyKey.holdResultsOpenOverStatementClose).getValue();

        this.resultSetFactory = new ResultSetFactory(this.connection, this);
    }

    protected void initQuery() {
        this.query = new SimpleQuery(this.session);
    }

    @Override
    public void addBatch(String sql) throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            if (sql != null) {
                this.query.addBatch(sql);
            }
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public void addBatch(Object batch) {
        this.query.addBatch(batch);
    }

    @Override
    public List<Object> getBatchedArgs() {
        return this.query.getBatchedArgs();
    }

    @Override
    public void cancel() throws SQLException {
        if (!this.query.getStatementExecuting().get()) {
            return;
        }

        if (!this.isClosed && this.connection != null) {
            NativeSession newSession = null;

            try {
                HostInfo hostInfo = this.session.getHostInfo();
                String database = hostInfo.getDatabase();
                String user = hostInfo.getUser();
                String password = hostInfo.getPassword();
                newSession = new NativeSession(this.session.getHostInfo(), this.session.getPropertySet());

                TelemetrySpan span = newSession.getTelemetryHandler().startSpan(TelemetrySpanName.CANCEL_QUERY);
                try (TelemetryScope scope = span.makeCurrent()) {
                    span.setAttribute(TelemetryAttribute.DB_NAME, database);
                    span.setAttribute(TelemetryAttribute.DB_OPERATION, TelemetryAttribute.OPERATION_KILL);
                    span.setAttribute(TelemetryAttribute.DB_STATEMENT, TelemetryAttribute.OPERATION_KILL + TelemetryAttribute.STATEMENT_SUFFIX);
                    span.setAttribute(TelemetryAttribute.DB_SYSTEM, TelemetryAttribute.DB_SYSTEM_DEFAULT);
                    span.setAttribute(TelemetryAttribute.DB_USER, user);
                    span.setAttribute(TelemetryAttribute.THREAD_ID, () -> Thread.currentThread().getId());
                    span.setAttribute(TelemetryAttribute.THREAD_NAME, () -> Thread.currentThread().getName());

                    newSession.connect(hostInfo, user, password, database, 30000, new TransactionEventHandler() {

                        @Override
                        public void transactionCompleted() {
                        }

                        @Override
                        public void transactionBegun() {
                        }

                    });
                    newSession.getProtocol().sendCommand(new NativeMessageBuilder(newSession.getServerSession().supportsQueryAttributes())
                            .buildComQuery(newSession.getSharedSendPacket(), newSession, "KILL QUERY " + this.session.getThreadId()), false, 0);
                    setCancelStatus(CancelStatus.CANCELED_BY_USER);
                } catch (Throwable t) {
                    span.setError(t);
                    throw t;
                } finally {
                    span.end();
                }
            } catch (IOException e) {
                throw SQLExceptionsMapping.translateException(e, this.exceptionInterceptor);
            } finally {
                if (newSession != null) {
                    newSession.forceClose();
                }
            }

        }
    }

    /**
     * Checks if closed() has been called, and throws an exception if so
     *
     * @return connection
     * @throws StatementIsClosedException
     *             if this statement has been closed
     */
    protected JdbcConnection checkClosed() {
        JdbcConnection c = this.connection;
        if (c == null) {
            throw ExceptionFactory.createException(StatementIsClosedException.class, Messages.getString("Statement.AlreadyClosed"), getExceptionInterceptor());
        }
        return c;
    }

    /**
     * Checks if the given SQL query is a result set producing query.
     *
     * @param sql
     *            the SQL to check
     * @return
     *         <code>true</code> if the query produces a result set, <code>false</code> otherwise.
     */
    protected boolean isResultSetProducingQuery(String sql) {
        QueryReturnType queryReturnType = QueryInfo.getQueryReturnType(sql, this.session.getServerSession().isNoBackslashEscapesSet());
        return queryReturnType == QueryReturnType.PRODUCES_RESULT_SET || queryReturnType == QueryReturnType.MAY_PRODUCE_RESULT_SET;
    }

    /**
     * Checks if the given SQL query does not return a result set.
     *
     * @param sql
     *            the SQL to check
     * @return
     *         <code>true</code> if the query does not produce a result set, <code>false</code> otherwise.
     */
    protected boolean isNonResultSetProducingQuery(String sql) {
        QueryReturnType queryReturnType = QueryInfo.getQueryReturnType(sql, this.session.getServerSession().isNoBackslashEscapesSet());
        return queryReturnType == QueryReturnType.DOES_NOT_PRODUCE_RESULT_SET || queryReturnType == QueryReturnType.MAY_PRODUCE_RESULT_SET
                || queryReturnType == QueryReturnType.NONE;
    }

    /**
     * Method checkNullOrEmptyQuery.
     *
     * @param sql
     *            the SQL to check
     *
     * @throws SQLException
     *             if query is null or empty.
     */
    protected void checkNullOrEmptyQuery(String sql) throws SQLException {
        if (sql == null) {
            throw SQLError.createSQLException(Messages.getString("Statement.59"), MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT, getExceptionInterceptor());
        }

        if (sql.length() == 0) {
            throw SQLError.createSQLException(Messages.getString("Statement.61"), MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT, getExceptionInterceptor());
        }
    }

    @Override
    public void clearBatch() throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            this.query.clearBatchedArgs();
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public void clearBatchedArgs() {
        this.query.clearBatchedArgs();
    }

    @Override
    public void clearWarnings() throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            setClearWarningsCalled(true);
            this.warningChain = null;
            // TODO souldn't we also clear warnings from _server_ ?
        } finally {
            connectionLock.unlock();
        }
    }

    /**
     * In many cases, it is desirable to immediately release a Statement's database and JDBC resources instead of waiting for this to happen when it is
     * automatically closed. The close method provides this immediate release.
     *
     * @exception SQLException
     *                if a database access error occurs
     */
    @Override
    public void close() throws SQLException {
        doClose(CloseOption.PROPAGATE);
    }

    /**
     * Close any open result sets that have been 'held open'
     *
     * @throws SQLException
     *             if an error occurs
     */
    protected void closeAllOpenResults() throws SQLException {
        JdbcConnection locallyScopedConn = this.connection;
        if (locallyScopedConn == null) {
            return; // already closed
        }

        Lock connectionLock = locallyScopedConn.getConnectionLock();
        connectionLock.lock();

        try {
            if (this.openResultSets != null) {
                this.openResultSets.forEach(rs -> {
                    try {
                        rs.doClose(CloseOption.IMPLICIT);
                    } catch (SQLException e) {
                        throw AssertionFailedException.shouldNotHappen(e);
                    }
                });
                this.openResultSets.clear();
            }
        } finally {
            connectionLock.unlock();
        }
    }

    /**
     * Close all result sets in this statement. This includes multi-results
     *
     * @throws SQLException
     *             if a database access error occurs
     */
    protected void implicitlyCloseAllOpenResults() throws SQLException {
        if (!(this.holdResultsOpenOverClose || this.dontTrackOpenResources.getValue())) {
            if (this.results != null) {
                this.results.doClose(CloseOption.IMPLICIT);
            }
            if (this.generatedKeysResults != null) {
                this.generatedKeysResults.doClose(CloseOption.IMPLICIT);
            }
            closeAllOpenResults();
            this.results = null;
            this.generatedKeysResults = null;
        }
    }

    @Override
    public void notifyResultSetClose(ResultSetInternalMethods rs) {
        try {
            Lock connectionLock = checkClosed().getConnectionLock();
            connectionLock.lock();
            try {
                if (this.openResultSets != null) {
                    this.openResultSets.remove(rs);
                }

                boolean hasMoreResults = rs.getNextResultset() != null;

                // clear the current results or generated keys results
                if (this.results == rs && !hasMoreResults) {
                    this.results = null;
                }
                if (this.generatedKeysResults == rs) {
                    this.generatedKeysResults = null;
                }

                // trigger closeOnCompletion if:
                // a) the result set close wasn't triggered internally
                // b) there are no additional results
                if (!hasMoreResults) {
                    checkAndPerformCloseOnCompletionAction();
                }
            } finally {
                connectionLock.unlock();
            }
        } catch (StatementIsClosedException e) {
            // we can't break the interface, having this be no-op in case of error is ok
        }
    }

    @Override
    public int getOpenResultSetCount() {
        try {
            Lock connectionLock = checkClosed().getConnectionLock();
            connectionLock.lock();
            try {
                return this.openResultSets != null ? this.openResultSets.size() : 0;
            } finally {
                connectionLock.unlock();
            }
        } catch (StatementIsClosedException e) {
            // we can't break the interface, having this be no-op in case of error is ok
            return 0;
        }
    }

    /**
     * Check if all ResultSets generated by this statement are closed. If so, close this statement.
     */
    private void checkAndPerformCloseOnCompletionAction() {
        try {
            Lock connectionLock = checkClosed().getConnectionLock();
            connectionLock.lock();
            try {
                if (isCloseOnCompletion() && !this.dontTrackOpenResources.getValue() && getOpenResultSetCount() == 0
                        && (this.results == null || !this.results.hasRows() || this.results.isClosed())
                        && (this.generatedKeysResults == null || !this.generatedKeysResults.hasRows() || this.generatedKeysResults.isClosed())) {
                    doClose(CloseOption.IMPLICIT);
                }
            } finally {
                connectionLock.unlock();
            }
        } catch (SQLException e) {
        }
    }

    /**
     * @param sql
     *            query
     * @return result set
     * @throws SQLException
     *             if a database access error occurs or this method is called on a closed Statement
     */
    private ResultSetInternalMethods createResultSetUsingServerFetch(String sql) throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            java.sql.PreparedStatement pStmt = this.connection.prepareStatement(sql, this.query.getResultType().getIntValue(), this.resultSetConcurrency);

            pStmt.setFetchSize(this.query.getResultFetchSize());

            if (getQueryTimeout() > 0) {
                pStmt.setQueryTimeout(getQueryTimeout());
            }

            if (this.maxRows > -1) {
                pStmt.setMaxRows(this.maxRows);
            }

            statementBegins();

            pStmt.execute();

            //
            // Need to be able to get resultset irrespective if we issued DML or not to make this work.
            //
            ResultSetInternalMethods rs = ((JdbcStatement) pStmt).getResultSetInternal();

            rs.setStatementUsedForFetchingRows((JdbcPreparedStatement) pStmt);

            this.results = rs;

            return rs;
        } finally {
            connectionLock.unlock();
        }
    }

    /**
     * We only stream result sets when they are forward-only, read-only, and the fetch size has been set to Integer.MIN_VALUE
     *
     * @return true if this result set should be streamed row at-a-time, rather than read all at once.
     */
    protected boolean meetsConditionsForStreamingResultSet() {
        return this.query.getResultType() == Type.FORWARD_ONLY && this.resultSetConcurrency == java.sql.ResultSet.CONCUR_READ_ONLY
                && this.query.getResultFetchSize() == Integer.MIN_VALUE;
    }

    private Resultset.Type originalResultSetType = Type.FORWARD_ONLY;
    private int originalFetchSize = 0;

    @Override
    public void enableStreamingResults() throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            this.originalResultSetType = this.query.getResultType();
            this.originalFetchSize = this.query.getResultFetchSize();

            setFetchSize(Integer.MIN_VALUE);
            setResultSetType(Type.FORWARD_ONLY);
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public void disableStreamingResults() throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            if (this.query.getResultFetchSize() == Integer.MIN_VALUE && this.query.getResultType() == Type.FORWARD_ONLY) {
                setFetchSize(this.originalFetchSize);
                setResultSetType(this.originalResultSetType);
            }
        } finally {
            connectionLock.unlock();
        }
    }

    /**
     * Adjust net_write_timeout to a higher value if we're streaming result sets. More often than not, someone runs into an issue where they blow
     * net_write_timeout when using this feature, and if they're willing to hold a result set open for 30 seconds or more, one more round-trip isn't going to
     * hurt.
     *
     * This is reset by ResultsetRowsStreaming.close().
     *
     * @param con
     *            created this statement
     * @throws SQLException
     *             if a database error occurs
     */
    protected void setupStreamingTimeout(JdbcConnection con) throws SQLException {
        int netTimeoutForStreamingResults = this.session.getPropertySet().getIntegerProperty(PropertyKey.netTimeoutForStreamingResults).getValue();
        if (meetsConditionsForStreamingResultSet() && netTimeoutForStreamingResults > 0) {
            TelemetrySpan span = this.session.getTelemetryHandler().startSpan(TelemetrySpanName.SET_VARIABLE, "net_write_timeout");
            try (TelemetryScope scope = span.makeCurrent()) {
                span.setAttribute(TelemetryAttribute.DB_NAME, this::getCurrentDatabase);
                span.setAttribute(TelemetryAttribute.DB_OPERATION, TelemetryAttribute.OPERATION_SET);
                span.setAttribute(TelemetryAttribute.DB_STATEMENT, TelemetryAttribute.OPERATION_SET + TelemetryAttribute.STATEMENT_SUFFIX);
                span.setAttribute(TelemetryAttribute.DB_SYSTEM, TelemetryAttribute.DB_SYSTEM_DEFAULT);
                span.setAttribute(TelemetryAttribute.DB_USER, () -> this.connection.getUser());
                span.setAttribute(TelemetryAttribute.THREAD_ID, () -> Thread.currentThread().getId());
                span.setAttribute(TelemetryAttribute.THREAD_NAME, () -> Thread.currentThread().getName());

                executeSimpleNonQuery(con, "SET net_write_timeout=" + netTimeoutForStreamingResults);
            } catch (Throwable t) {
                span.setError(t);
                throw t;
            } finally {
                span.end();
            }
        }
    }

    @Override
    public CancelQueryTask startQueryTimer(Query stmtToCancel, long timeout) {
        return this.query.startQueryTimer(stmtToCancel, timeout);
    }

    @Override
    public void stopQueryTimer(CancelQueryTask timeoutTask, boolean rethrowCancelReason, boolean checkCancelTimeout) {
        this.query.stopQueryTimer(timeoutTask, rethrowCancelReason, checkCancelTimeout);
    }

    @Override
    public boolean execute(String sql) throws SQLException {
        return executeInternal(sql, false);
    }

    @Override
    public boolean execute(String sql, int returnGeneratedKeys) throws SQLException {
        return executeInternal(sql, returnGeneratedKeys == java.sql.Statement.RETURN_GENERATED_KEYS);
    }

    @Override
    public boolean execute(String sql, int[] generatedKeyIndices) throws SQLException {
        return executeInternal(sql, generatedKeyIndices != null && generatedKeyIndices.length > 0);
    }

    @Override
    public boolean execute(String sql, String[] generatedKeyNames) throws SQLException {
        return executeInternal(sql, generatedKeyNames != null && generatedKeyNames.length > 0);
    }

    private boolean executeInternal(String sql, boolean returnGeneratedKeys) throws SQLException {
        JdbcConnection locallyScopedConn = checkClosed();

        Lock connectionLock = locallyScopedConn.getConnectionLock();
        connectionLock.lock();
        try {
            checkClosed();

            TelemetrySpan span = getSession().getTelemetryHandler().startSpan(TelemetrySpanName.STMT_EXECUTE);
            try (TelemetryScope scope = span.makeCurrent()) {
                String dbOperation = QueryInfo.getStatementKeyword(sql, this.session.getServerSession().isNoBackslashEscapesSet());
                span.setAttribute(TelemetryAttribute.DB_NAME, this::getCurrentDatabase);
                span.setAttribute(TelemetryAttribute.DB_OPERATION, dbOperation);
                span.setAttribute(TelemetryAttribute.DB_STATEMENT, dbOperation + TelemetryAttribute.STATEMENT_SUFFIX);
                span.setAttribute(TelemetryAttribute.DB_SYSTEM, TelemetryAttribute.DB_SYSTEM_DEFAULT);
                span.setAttribute(TelemetryAttribute.DB_USER, () -> this.connection.getUser());
                span.setAttribute(TelemetryAttribute.THREAD_ID, () -> Thread.currentThread().getId());
                span.setAttribute(TelemetryAttribute.THREAD_NAME, () -> Thread.currentThread().getName());
                checkNullOrEmptyQuery(sql);

                resetCancelledState();

                implicitlyCloseAllOpenResults();

                clearWarnings();

                if (sql.charAt(0) == '/') {
                    if (sql.startsWith(PING_MARKER)) {
                        doPingInstead();

                        return true;
                    }
                }

                this.retrieveGeneratedKeys = returnGeneratedKeys;

                this.lastQueryIsOnDupKeyUpdate = returnGeneratedKeys
                        && QueryInfo.firstCharOfStatementUc(sql, this.session.getServerSession().isNoBackslashEscapesSet()) == 'I'
                        && containsOnDuplicateKeyInString(sql);

                if (!QueryInfo.isReadOnlySafeQuery(sql, this.session.getServerSession().isNoBackslashEscapesSet()) && locallyScopedConn.isReadOnly()) {
                    throw SQLError.createSQLException(Messages.getString("Statement.27") + Messages.getString("Statement.28"),
                            MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT, getExceptionInterceptor());
                }

                try {
                    setupStreamingTimeout(locallyScopedConn);

                    if (this.doEscapeProcessing) {
                        Object escapedSqlResult = EscapeProcessor.escapeSQL(sql, this.session.getServerSession().getSessionTimeZone(),
                                this.session.getServerSession().getCapabilities().serverSupportsFracSecs(),
                                this.session.getServerSession().isServerTruncatesFracSecs(), getExceptionInterceptor());
                        sql = escapedSqlResult instanceof String ? (String) escapedSqlResult : ((EscapeProcessorResult) escapedSqlResult).escapedSql;
                    }

                    CachedResultSetMetaData cachedMetaData = null;

                    ResultSetInternalMethods rs = null;

                    this.batchedGeneratedKeys = null;

                    if (useServerFetch()) {
                        rs = createResultSetUsingServerFetch(sql);
                    } else {
                        CancelQueryTask timeoutTask = null;

                        String oldDb = null;

                        try {
                            timeoutTask = startQueryTimer(this, getTimeoutInMillis());

                            if (!locallyScopedConn.getDatabase().equals(getCurrentDatabase())) {
                                oldDb = locallyScopedConn.getDatabase();
                                locallyScopedConn.setDatabase(getCurrentDatabase());
                            }

                            // Check if we have cached metadata for this query...
                            if (locallyScopedConn.getPropertySet().getBooleanProperty(PropertyKey.cacheResultSetMetadata).getValue()) {
                                cachedMetaData = locallyScopedConn.getCachedMetaData(sql);
                            }

                            // Only apply max_rows to selects
                            locallyScopedConn.setSessionMaxRows(isResultSetProducingQuery(sql) ? this.maxRows : -1);

                            statementBegins();

                            rs = ((NativeSession) locallyScopedConn.getSession()).execSQL(this, sql, this.maxRows, null, meetsConditionsForStreamingResultSet(),
                                    getResultSetFactory(), cachedMetaData, false);

                            if (timeoutTask != null) {
                                stopQueryTimer(timeoutTask, true, true);
                                timeoutTask = null;
                            }

                        } catch (CJTimeoutException | OperationCancelledException e) {
                            throw SQLExceptionsMapping.translateException(e, this.exceptionInterceptor);

                        } finally {
                            stopQueryTimer(timeoutTask, false, false);

                            if (oldDb != null) {
                                locallyScopedConn.setDatabase(oldDb);
                            }
                        }
                    }

                    if (rs != null) {
                        this.lastInsertId = rs.getUpdateID();

                        this.results = rs;

                        rs.setFirstCharOfQuery(QueryInfo.firstCharOfStatementUc(sql, this.session.getServerSession().isNoBackslashEscapesSet()));

                        if (rs.hasRows()) {
                            if (cachedMetaData != null) {
                                locallyScopedConn.initializeResultsMetadataFromCache(sql, cachedMetaData, this.results);
                            } else if (this.session.getPropertySet().getBooleanProperty(PropertyKey.cacheResultSetMetadata).getValue()) {
                                locallyScopedConn.initializeResultsMetadataFromCache(sql, null /* will be created */, this.results);
                            }
                        }
                    }

                    return rs != null && rs.hasRows();
                } finally {
                    this.query.getStatementExecuting().set(false);
                }
            } catch (Throwable t) {
                span.setError(t);
                throw t;
            } finally {
                span.end();
            }
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public void statementBegins() {
        this.query.statementBegins();
    }

    @Override
    public void resetCancelledState() {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            this.query.resetCancelledState();
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public int[] executeBatch() throws SQLException {
        return Util.truncateAndConvertToInt(executeBatchInternal());
    }

    protected long[] executeBatchInternal() throws SQLException {
        JdbcConnection locallyScopedConn = checkClosed();

        Lock connectionLock = locallyScopedConn.getConnectionLock();
        connectionLock.lock();
        try {
            TelemetrySpan span = getSession().getTelemetryHandler().startSpan(TelemetrySpanName.STMT_EXECUTE_BATCH);
            try (TelemetryScope scope = span.makeCurrent()) {
                span.setAttribute(TelemetryAttribute.DB_NAME, this::getCurrentDatabase);
                span.setAttribute(TelemetryAttribute.DB_OPERATION, TelemetryAttribute.OPERATION_BATCH);
                span.setAttribute(TelemetryAttribute.DB_STATEMENT, TelemetryAttribute.OPERATION_BATCH);
                span.setAttribute(TelemetryAttribute.DB_SYSTEM, TelemetryAttribute.DB_SYSTEM_DEFAULT);
                span.setAttribute(TelemetryAttribute.DB_USER, () -> this.connection.getUser());
                span.setAttribute(TelemetryAttribute.THREAD_ID, () -> Thread.currentThread().getId());
                span.setAttribute(TelemetryAttribute.THREAD_NAME, () -> Thread.currentThread().getName());

                if (locallyScopedConn.isReadOnly()) {
                    throw SQLError.createSQLException(Messages.getString("Statement.34") + Messages.getString("Statement.35"),
                            MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT, getExceptionInterceptor());
                }

                implicitlyCloseAllOpenResults();

                List<Object> batchedArgs = this.query.getBatchedArgs();

                if (batchedArgs == null || batchedArgs.size() == 0) {
                    return new long[0];
                }

                // we timeout the entire batch, not individual statements
                long individualStatementTimeout = getTimeoutInMillis();
                setTimeoutInMillis(0);

                CancelQueryTask timeoutTask = null;

                try {
                    resetCancelledState();

                    statementBegins();

                    clearWarnings();

                    try {
                        this.retrieveGeneratedKeys = true; // The JDBC spec doesn't forbid this, but doesn't provide for it either...we do..

                        long[] updateCounts = null;

                        if (batchedArgs != null) {
                            int nbrCommands = batchedArgs.size();
                            this.batchedGeneratedKeys = new ArrayList<>(nbrCommands);

                            if (this.rewriteBatchedStatements.getValue() && nbrCommands > 4) {
                                return executeBatchUsingMultiQueries(individualStatementTimeout);
                            }

                            timeoutTask = startQueryTimer(this, individualStatementTimeout);

                            updateCounts = new long[nbrCommands];

                            for (int i = 0; i < nbrCommands; i++) {
                                updateCounts[i] = -3;
                            }

                            SQLException sqlEx = null;

                            int commandIndex = 0;

                            for (commandIndex = 0; commandIndex < nbrCommands; commandIndex++) {
                                try {
                                    String sql = (String) batchedArgs.get(commandIndex);
                                    updateCounts[commandIndex] = executeUpdateInternal(sql, true, true);

                                    if (timeoutTask != null) {
                                        // we need to check the cancel state on each iteration to generate timeout exception if needed
                                        checkCancelTimeout();
                                    }

                                    // limit one generated key per OnDuplicateKey statement
                                    getBatchedGeneratedKeys(this.results.getFirstCharOfQuery() == 'I' && containsOnDuplicateKeyInString(sql) ? 1 : 0);
                                } catch (SQLException ex) {
                                    updateCounts[commandIndex] = EXECUTE_FAILED;

                                    if (this.continueBatchOnError && !(ex instanceof MySQLTimeoutException) && !(ex instanceof MySQLStatementCancelledException)
                                            && !hasDeadlockOrTimeoutRolledBackTx(ex)) {
                                        sqlEx = ex;
                                    } else {
                                        long[] newUpdateCounts = new long[commandIndex];

                                        if (hasDeadlockOrTimeoutRolledBackTx(ex)) {
                                            for (int i = 0; i < newUpdateCounts.length; i++) {
                                                newUpdateCounts[i] = java.sql.Statement.EXECUTE_FAILED;
                                            }
                                        } else {
                                            System.arraycopy(updateCounts, 0, newUpdateCounts, 0, commandIndex);
                                        }

                                        sqlEx = ex;
                                        break;
                                        //throw SQLError.createBatchUpdateException(ex, newUpdateCounts, getExceptionInterceptor());
                                    }
                                }
                            }

                            if (sqlEx != null) {
                                throw SQLError.createBatchUpdateException(sqlEx, updateCounts, getExceptionInterceptor());
                            }
                        }

                        if (timeoutTask != null) {
                            stopQueryTimer(timeoutTask, true, true);
                            timeoutTask = null;
                        }

                        return updateCounts != null ? updateCounts : new long[0];
                    } finally {
                        this.query.getStatementExecuting().set(false);
                    }
                } finally {

                    stopQueryTimer(timeoutTask, false, false);
                    resetCancelledState();

                    setTimeoutInMillis(individualStatementTimeout);

                    clearBatch();
                }
            } catch (Throwable t) {
                span.setError(t);
                throw t;
            } finally {
                span.end();
            }
        } finally {
            connectionLock.unlock();
        }
    }

    protected final boolean hasDeadlockOrTimeoutRolledBackTx(SQLException ex) {
        int vendorCode = ex.getErrorCode();

        switch (vendorCode) {
            case MysqlErrorNumbers.ER_LOCK_DEADLOCK:
            case MysqlErrorNumbers.ER_LOCK_TABLE_FULL:
                return true;
            case MysqlErrorNumbers.ER_LOCK_WAIT_TIMEOUT:
                return false;
            default:
                return false;
        }
    }

    /**
     * Rewrites batch into a single query to send to the server. This method will constrain each batch to be shorter than max_allowed_packet on the server.
     *
     * @param individualStatementTimeout
     *            timeout for a single query in a batch
     *
     * @return update counts in the same manner as executeBatch()
     * @throws SQLException
     *             if a database access error occurs or this method is called on a closed PreparedStatement
     */
    private long[] executeBatchUsingMultiQueries(long individualStatementTimeout) throws SQLException {
        JdbcConnection locallyScopedConn = checkClosed();

        Lock connectionLock = locallyScopedConn.getConnectionLock();
        connectionLock.lock();
        try {
            boolean multiQueriesEnabled = locallyScopedConn.getPropertySet().getBooleanProperty(PropertyKey.allowMultiQueries).getValue();
            if (!multiQueriesEnabled) {
                this.session.enableMultiQueries(); // Temporarily enable multi-queries.
            }

            java.sql.Statement batchStmt = null;
            CancelQueryTask timeoutTask = null;
            try {
                List<Object> batchedArgs = this.query.getBatchedArgs();
                int nbrCommands = batchedArgs.size();
                long[] updateCounts = new long[nbrCommands];
                for (int i = 0; i < nbrCommands; i++) {
                    updateCounts[i] = Statement.EXECUTE_FAILED;
                }

                int commandIndex = 0;

                StringBuilder queryBuf = new StringBuilder();

                batchStmt = locallyScopedConn.createStatement();
                JdbcStatement jdbcBatchedStmt = (JdbcStatement) batchStmt;
                getQueryAttributesBindings().runThroughAll(a -> jdbcBatchedStmt.setAttribute(a.getName(), a.getValue()));

                timeoutTask = startQueryTimer((StatementImpl) batchStmt, individualStatementTimeout);

                int counter = 0;

                String connectionEncoding = locallyScopedConn.getPropertySet().getStringProperty(PropertyKey.characterEncoding).getValue();

                int numberOfBytesPerChar = StringUtils.startsWithIgnoreCase(connectionEncoding, "utf") ? 3
                        : this.session.getServerSession().getCharsetSettings().isMultibyteCharset(connectionEncoding) ? 2 : 1;

                int escapeAdjust = 1;

                batchStmt.setEscapeProcessing(this.doEscapeProcessing);
                if (this.doEscapeProcessing) {
                    escapeAdjust = 2; // We assume packet _could_ grow by this amount, as we're not sure how big statement will end up after escape processing
                }

                SQLException sqlEx = null;

                int argumentSetsInBatchSoFar = 0;
                boolean batchContainsResultSetProducingQuery = false;
                for (commandIndex = 0; commandIndex < nbrCommands; commandIndex++) {
                    String nextQuery = (String) this.query.getBatchedArgs().get(commandIndex);

                    if (queryBuf.length() > 0 && ((queryBuf.length() + nextQuery.length()) * numberOfBytesPerChar + 1 /* for semicolon */
                            + NativeConstants.HEADER_LENGTH) * escapeAdjust + 32 > this.maxAllowedPacket.getValue()) {
                        try {
                            if (batchContainsResultSetProducingQuery) {
                                throw SQLError.createSQLException(Messages.getString("Statement.46"), "01S03", getExceptionInterceptor());
                            }
                            batchStmt.executeUpdate(queryBuf.toString(), java.sql.Statement.RETURN_GENERATED_KEYS);
                        } catch (SQLException ex) {
                            sqlEx = handleExceptionForBatch(commandIndex, argumentSetsInBatchSoFar, updateCounts, ex);
                        }

                        counter = processMultiCountsAndKeys((StatementImpl) batchStmt, counter, updateCounts);

                        queryBuf = new StringBuilder();
                        argumentSetsInBatchSoFar = 0;
                        batchContainsResultSetProducingQuery = false;
                    }

                    batchContainsResultSetProducingQuery = batchContainsResultSetProducingQuery || !isNonResultSetProducingQuery(nextQuery);
                    queryBuf.append(nextQuery);
                    queryBuf.append(";");
                    argumentSetsInBatchSoFar++;
                }

                if (queryBuf.length() > 0) {
                    try {
                        if (batchContainsResultSetProducingQuery) {
                            throw SQLError.createSQLException(Messages.getString("Statement.46"), "01S03", getExceptionInterceptor());
                        }
                        batchStmt.executeUpdate(queryBuf.toString(), java.sql.Statement.RETURN_GENERATED_KEYS);
                    } catch (SQLException ex) {
                        sqlEx = handleExceptionForBatch(commandIndex - 1, argumentSetsInBatchSoFar, updateCounts, ex);
                    }

                    counter = processMultiCountsAndKeys((StatementImpl) batchStmt, counter, updateCounts);
                }

                if (timeoutTask != null) {
                    stopQueryTimer(timeoutTask, true, true);
                    timeoutTask = null;
                }

                if (sqlEx != null) {
                    throw SQLError.createBatchUpdateException(sqlEx, updateCounts, getExceptionInterceptor());
                }

                return updateCounts != null ? updateCounts : new long[0];
            } finally {
                stopQueryTimer(timeoutTask, false, false);
                resetCancelledState();

                try {
                    if (batchStmt != null) {
                        batchStmt.close();
                    }
                } finally {
                    if (!multiQueriesEnabled) {
                        this.session.disableMultiQueries();
                    }
                }
            }
        } finally {
            connectionLock.unlock();
        }
    }

    protected int processMultiCountsAndKeys(StatementImpl batchedStatement, int updateCountCounter, long[] updateCounts) throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            updateCounts[updateCountCounter++] = batchedStatement.getLargeUpdateCount();
            this.updateCount = batchedStatement.getLargeUpdateCount();

            boolean doGenKeys = this.batchedGeneratedKeys != null;

            byte[][] row = null;

            if (doGenKeys) {
                long generatedKey = batchedStatement.getLastInsertID();
                if (generatedKey != 0) {
                    row = new byte[1][];
                    row[0] = StringUtils.getBytes(Long.toString(generatedKey));
                    this.batchedGeneratedKeys.add(new ByteArrayRow(row, getExceptionInterceptor()));
                }
            }

            while (batchedStatement.getMoreResults() || batchedStatement.getLargeUpdateCount() != -1) {
                updateCounts[updateCountCounter++] = batchedStatement.getLargeUpdateCount();
                this.updateCount = batchedStatement.getLargeUpdateCount();

                if (doGenKeys) {
                    long generatedKey = batchedStatement.getLastInsertID();
                    if (generatedKey != 0) {
                        row = new byte[1][];
                        row[0] = StringUtils.getBytes(Long.toString(generatedKey));
                        this.batchedGeneratedKeys.add(new ByteArrayRow(row, getExceptionInterceptor()));
                    }
                }
            }

            return updateCountCounter;
        } finally {
            connectionLock.unlock();
        }
    }

    protected SQLException handleExceptionForBatch(int endOfBatchIndex, int numValuesPerBatch, long[] updateCounts, SQLException ex)
            throws BatchUpdateException, SQLException {
        for (int j = endOfBatchIndex; j > endOfBatchIndex - numValuesPerBatch; j--) {
            updateCounts[j] = EXECUTE_FAILED;
        }

        if (this.continueBatchOnError && !(ex instanceof MySQLTimeoutException) && !(ex instanceof MySQLStatementCancelledException)
                && !hasDeadlockOrTimeoutRolledBackTx(ex)) {
            return ex;
        } // else: throw the exception immediately

        long[] newUpdateCounts = new long[endOfBatchIndex];
        System.arraycopy(updateCounts, 0, newUpdateCounts, 0, endOfBatchIndex);

        throw SQLError.createBatchUpdateException(ex, newUpdateCounts, getExceptionInterceptor());
    }

    @Override
    public java.sql.ResultSet executeQuery(String sql) throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            TelemetrySpan span = getSession().getTelemetryHandler().startSpan(TelemetrySpanName.STMT_EXECUTE);
            try (TelemetryScope scope = span.makeCurrent()) {
                String dbOperation = QueryInfo.getStatementKeyword(sql, this.session.getServerSession().isNoBackslashEscapesSet());
                span.setAttribute(TelemetryAttribute.DB_NAME, this::getCurrentDatabase);
                span.setAttribute(TelemetryAttribute.DB_OPERATION, dbOperation);
                span.setAttribute(TelemetryAttribute.DB_STATEMENT, dbOperation + TelemetryAttribute.STATEMENT_SUFFIX);
                span.setAttribute(TelemetryAttribute.DB_SYSTEM, TelemetryAttribute.DB_SYSTEM_DEFAULT);
                span.setAttribute(TelemetryAttribute.DB_USER, () -> this.connection.getUser());
                span.setAttribute(TelemetryAttribute.THREAD_ID, () -> Thread.currentThread().getId());
                span.setAttribute(TelemetryAttribute.THREAD_NAME, () -> Thread.currentThread().getName());

                JdbcConnection locallyScopedConn = this.connection;

                this.retrieveGeneratedKeys = false;

                checkNullOrEmptyQuery(sql);

                resetCancelledState();

                implicitlyCloseAllOpenResults();

                clearWarnings();

                if (sql.charAt(0) == '/') {
                    if (sql.startsWith(PING_MARKER)) {
                        doPingInstead();

                        return this.results;
                    }
                }

                setupStreamingTimeout(locallyScopedConn);

                if (this.doEscapeProcessing) {
                    Object escapedSqlResult = EscapeProcessor.escapeSQL(sql, this.session.getServerSession().getSessionTimeZone(),
                            this.session.getServerSession().getCapabilities().serverSupportsFracSecs(),
                            this.session.getServerSession().isServerTruncatesFracSecs(), getExceptionInterceptor());
                    sql = escapedSqlResult instanceof String ? (String) escapedSqlResult : ((EscapeProcessorResult) escapedSqlResult).escapedSql;
                }

                if (!isResultSetProducingQuery(sql)) {
                    throw SQLError.createSQLException(Messages.getString("Statement.57"), MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT,
                            getExceptionInterceptor());
                }

                CachedResultSetMetaData cachedMetaData = null;

                if (useServerFetch()) {
                    this.results = createResultSetUsingServerFetch(sql);

                    return this.results;
                }

                CancelQueryTask timeoutTask = null;

                String oldDb = null;

                try {
                    timeoutTask = startQueryTimer(this, getTimeoutInMillis());

                    if (!locallyScopedConn.getDatabase().equals(getCurrentDatabase())) {
                        oldDb = locallyScopedConn.getDatabase();
                        locallyScopedConn.setDatabase(getCurrentDatabase());
                    }

                    //
                    // Check if we have cached metadata for this query...
                    //
                    if (locallyScopedConn.getPropertySet().getBooleanProperty(PropertyKey.cacheResultSetMetadata).getValue()) {
                        cachedMetaData = locallyScopedConn.getCachedMetaData(sql);
                    }

                    locallyScopedConn.setSessionMaxRows(this.maxRows);

                    statementBegins();

                    this.results = ((NativeSession) locallyScopedConn.getSession()).execSQL(this, sql, this.maxRows, null,
                            meetsConditionsForStreamingResultSet(), getResultSetFactory(), cachedMetaData, false);

                    if (timeoutTask != null) {
                        stopQueryTimer(timeoutTask, true, true);
                        timeoutTask = null;
                    }

                } catch (CJTimeoutException | OperationCancelledException e) {
                    throw SQLExceptionsMapping.translateException(e, this.exceptionInterceptor);

                } finally {
                    this.query.getStatementExecuting().set(false);

                    stopQueryTimer(timeoutTask, false, false);

                    if (oldDb != null) {
                        locallyScopedConn.setDatabase(oldDb);
                    }
                }

                this.lastInsertId = this.results.getUpdateID();

                if (cachedMetaData != null) {
                    locallyScopedConn.initializeResultsMetadataFromCache(sql, cachedMetaData, this.results);
                } else if (this.connection.getPropertySet().getBooleanProperty(PropertyKey.cacheResultSetMetadata).getValue()) {
                    locallyScopedConn.initializeResultsMetadataFromCache(sql, null /* will be created */, this.results);
                }
            } catch (Throwable t) {
                span.setError(t);
                throw t;
            } finally {
                span.end();
            }

            return this.results;
        } finally {
            connectionLock.unlock();
        }
    }

    protected void doPingInstead() throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            if (this.pingTarget != null) {
                try {
                    this.pingTarget.doPing();
                } catch (SQLException e) {
                    throw e;
                } catch (Exception e) {
                    throw SQLError.createSQLException(e.getMessage(), MysqlErrorNumbers.SQLSTATE_MYSQL_COMMUNICATION_LINK_FAILURE, e,
                            getExceptionInterceptor());
                }
            } else {
                this.connection.ping();
            }

            ResultSetInternalMethods fakeSelectOneResultSet = generatePingResultSet();
            this.results = fakeSelectOneResultSet;
        } finally {
            connectionLock.unlock();
        }
    }

    protected ResultSetInternalMethods generatePingResultSet() throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            String encoding = this.session.getServerSession().getCharsetSettings().getMetadataEncoding();
            int collationIndex = this.session.getServerSession().getCharsetSettings().getMetadataCollationIndex();
            Field[] fields = { new Field(null, "1", collationIndex, encoding, MysqlType.BIGINT, 1) };
            ArrayList<Row> rows = new ArrayList<>();
            byte[] colVal = new byte[] { (byte) '1' };

            rows.add(new ByteArrayRow(new byte[][] { colVal }, getExceptionInterceptor()));

            return this.resultSetFactory.createFromResultsetRows(ResultSet.CONCUR_READ_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE,
                    new ResultsetRowsStatic(rows, new DefaultColumnDefinition(fields)));
        } finally {
            connectionLock.unlock();
        }
    }

    public void executeSimpleNonQuery(JdbcConnection c, String nonQuery) throws SQLException {
        Lock connectionLock = c.getConnectionLock();
        connectionLock.lock();
        try {
            ((NativeSession) c.getSession()).<ResultSetImpl>execSQL(this, nonQuery, -1, null, false, getResultSetFactory(), null, false).doClose();
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public int executeUpdate(String sql) throws SQLException {
        return Util.truncateAndConvertToInt(executeLargeUpdate(sql));
    }

    @Override
    public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
        return Util.truncateAndConvertToInt(executeLargeUpdate(sql, autoGeneratedKeys));
    }

    @Override
    public int executeUpdate(String sql, int[] columnIndexes) throws SQLException {
        return Util.truncateAndConvertToInt(executeLargeUpdate(sql, columnIndexes));
    }

    @Override
    public int executeUpdate(String sql, String[] columnNames) throws SQLException {
        return Util.truncateAndConvertToInt(executeLargeUpdate(sql, columnNames));
    }

    protected long executeUpdateInternal(String sql, boolean isBatch, boolean returnGeneratedKeys) throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            TelemetrySpan span = getSession().getTelemetryHandler().startSpan(TelemetrySpanName.STMT_EXECUTE);
            try (TelemetryScope scope = span.makeCurrent()) {
                String dbOperation = QueryInfo.getStatementKeyword(sql, this.session.getServerSession().isNoBackslashEscapesSet());
                span.setAttribute(TelemetryAttribute.DB_NAME, this::getCurrentDatabase);
                span.setAttribute(TelemetryAttribute.DB_OPERATION, dbOperation);
                span.setAttribute(TelemetryAttribute.DB_STATEMENT, dbOperation + TelemetryAttribute.STATEMENT_SUFFIX);
                span.setAttribute(TelemetryAttribute.DB_SYSTEM, TelemetryAttribute.DB_SYSTEM_DEFAULT);
                span.setAttribute(TelemetryAttribute.DB_USER, () -> this.connection.getUser());
                span.setAttribute(TelemetryAttribute.THREAD_ID, () -> Thread.currentThread().getId());
                span.setAttribute(TelemetryAttribute.THREAD_NAME, () -> Thread.currentThread().getName());

                JdbcConnection locallyScopedConn = this.connection;

                checkNullOrEmptyQuery(sql);

                resetCancelledState();

                clearWarnings();

                char firstStatementChar = QueryInfo.firstCharOfStatementUc(sql, this.session.getServerSession().isNoBackslashEscapesSet());
                if (!isNonResultSetProducingQuery(sql)) {
                    throw SQLError.createSQLException(Messages.getString("Statement.46"), "01S03", getExceptionInterceptor());
                }

                this.retrieveGeneratedKeys = returnGeneratedKeys;

                this.lastQueryIsOnDupKeyUpdate = returnGeneratedKeys && firstStatementChar == 'I' && containsOnDuplicateKeyInString(sql);

                ResultSetInternalMethods rs = null;
                if (!isBatch) {
                    this.batchedGeneratedKeys = null;
                }

                if (this.doEscapeProcessing) {
                    Object escapedSqlResult = EscapeProcessor.escapeSQL(sql, this.session.getServerSession().getSessionTimeZone(),
                            this.session.getServerSession().getCapabilities().serverSupportsFracSecs(),
                            this.session.getServerSession().isServerTruncatesFracSecs(), getExceptionInterceptor());
                    sql = escapedSqlResult instanceof String ? (String) escapedSqlResult : ((EscapeProcessorResult) escapedSqlResult).escapedSql;
                }

                if (locallyScopedConn.isReadOnly(false)) {
                    throw SQLError.createSQLException(Messages.getString("Statement.42") + Messages.getString("Statement.43"),
                            MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT, getExceptionInterceptor());
                }

                implicitlyCloseAllOpenResults();

                // The checking and changing of databases must happen in sequence, so synchronize on the same mutex that _conn is using

                CancelQueryTask timeoutTask = null;

                String oldDb = null;

                try {
                    timeoutTask = startQueryTimer(this, getTimeoutInMillis());

                    if (!locallyScopedConn.getDatabase().equals(getCurrentDatabase())) {
                        oldDb = locallyScopedConn.getDatabase();
                        locallyScopedConn.setDatabase(getCurrentDatabase());
                    }

                    //
                    // Only apply max_rows to selects
                    //
                    locallyScopedConn.setSessionMaxRows(-1);

                    statementBegins();

                    // null database: force read of field info on DML
                    rs = ((NativeSession) locallyScopedConn.getSession()).execSQL(this, sql, -1, null, false, getResultSetFactory(), null, isBatch);

                    if (timeoutTask != null) {
                        stopQueryTimer(timeoutTask, true, true);
                        timeoutTask = null;
                    }

                } catch (CJTimeoutException | OperationCancelledException e) {
                    throw SQLExceptionsMapping.translateException(e, this.exceptionInterceptor);

                } finally {
                    stopQueryTimer(timeoutTask, false, false);

                    if (oldDb != null) {
                        locallyScopedConn.setDatabase(oldDb);
                    }

                    if (!isBatch) {
                        this.query.getStatementExecuting().set(false);
                    }
                }

                this.results = rs;

                rs.setFirstCharOfQuery(firstStatementChar);

                this.updateCount = rs.getUpdateCount();

                this.lastInsertId = rs.getUpdateID();
            } catch (Throwable t) {
                span.setError(t);
                throw t;
            } finally {
                span.end();
            }

            return this.updateCount;
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public java.sql.Connection getConnection() throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            return this.connection;
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public int getFetchDirection() throws SQLException {
        return java.sql.ResultSet.FETCH_FORWARD;
    }

    @Override
    public int getFetchSize() throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            return this.query.getResultFetchSize();
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public java.sql.ResultSet getGeneratedKeys() throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            if (!this.retrieveGeneratedKeys) {
                throw SQLError.createSQLException(Messages.getString("Statement.GeneratedKeysNotRequested"), MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT,
                        getExceptionInterceptor());
            }

            if (this.batchedGeneratedKeys == null) {
                if (this.lastQueryIsOnDupKeyUpdate) {
                    return this.generatedKeysResults = getGeneratedKeysInternal(1);
                }
                return this.generatedKeysResults = getGeneratedKeysInternal();
            }

            String encoding = this.session.getServerSession().getCharsetSettings().getMetadataEncoding();
            int collationIndex = this.session.getServerSession().getCharsetSettings().getMetadataCollationIndex();
            Field[] fields = new Field[1];
            fields[0] = new Field("", "GENERATED_KEY", collationIndex, encoding, MysqlType.BIGINT_UNSIGNED, 20);

            this.generatedKeysResults = this.resultSetFactory.createFromResultsetRows(ResultSet.CONCUR_READ_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE,
                    new ResultsetRowsStatic(this.batchedGeneratedKeys, new DefaultColumnDefinition(fields)));

            return this.generatedKeysResults;
        } finally {
            connectionLock.unlock();
        }
    }

    /*
     * Needed because there's no concept of super.super to get to this
     * implementation from ServerPreparedStatement when dealing with batched
     * updates.
     */
    protected ResultSetInternalMethods getGeneratedKeysInternal() throws SQLException {
        long numKeys = getLargeUpdateCount();
        return getGeneratedKeysInternal(numKeys);
    }

    protected ResultSetInternalMethods getGeneratedKeysInternal(long numKeys) throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            String encoding = this.session.getServerSession().getCharsetSettings().getMetadataEncoding();
            int collationIndex = this.session.getServerSession().getCharsetSettings().getMetadataCollationIndex();
            Field[] fields = new Field[1];
            fields[0] = new Field("", "GENERATED_KEY", collationIndex, encoding, MysqlType.BIGINT_UNSIGNED, 20);

            ArrayList<Row> rowSet = new ArrayList<>();

            long beginAt = getLastInsertID();

            if (this.results != null) {
                String serverInfo = this.results.getServerInfo();

                //
                // Only parse server info messages for 'REPLACE' queries
                //
                if (numKeys > 0 && this.results.getFirstCharOfQuery() == 'R' && serverInfo != null && serverInfo.length() > 0) {
                    numKeys = getRecordCountFromInfo(serverInfo);
                }

                if (beginAt != 0 /* BIGINT UNSIGNED can wrap the protocol representation */ && numKeys > 0) {
                    for (int i = 0; i < numKeys; i++) {
                        byte[][] row = new byte[1][];
                        if (beginAt > 0) {
                            row[0] = StringUtils.getBytes(Long.toString(beginAt));
                        } else {
                            byte[] asBytes = new byte[8];
                            asBytes[7] = (byte) (beginAt & 0xff);
                            asBytes[6] = (byte) (beginAt >>> 8);
                            asBytes[5] = (byte) (beginAt >>> 16);
                            asBytes[4] = (byte) (beginAt >>> 24);
                            asBytes[3] = (byte) (beginAt >>> 32);
                            asBytes[2] = (byte) (beginAt >>> 40);
                            asBytes[1] = (byte) (beginAt >>> 48);
                            asBytes[0] = (byte) (beginAt >>> 56);

                            BigInteger val = new BigInteger(1, asBytes);

                            row[0] = val.toString().getBytes();
                        }
                        rowSet.add(new ByteArrayRow(row, getExceptionInterceptor()));
                        beginAt += this.connection.getAutoIncrementIncrement();
                    }
                }
            }

            ResultSetImpl gkRs = this.resultSetFactory.createFromResultsetRows(ResultSet.CONCUR_READ_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE,
                    new ResultsetRowsStatic(rowSet, new DefaultColumnDefinition(fields)));

            return gkRs;
        } finally {
            connectionLock.unlock();
        }
    }

    /**
     * getLastInsertID returns the value of the auto_incremented key after an
     * executeQuery() or execute() call.
     *
     * <p>
     * This gets around the un-threadsafe behavior of "select LAST_INSERT_ID()" which is tied to the Connection that created this Statement, and therefore could
     * have had many INSERTS performed before one gets a chance to call "select LAST_INSERT_ID()".
     * </p>
     *
     * @return the last update ID.
     */
    public long getLastInsertID() {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            return this.lastInsertId;
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public int getMaxFieldSize() throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            return this.maxFieldSize;
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public int getMaxRows() throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            if (this.maxRows <= 0) {
                return 0;
            }

            return this.maxRows;
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public boolean getMoreResults() throws SQLException {
        return getMoreResults(CLOSE_CURRENT_RESULT);
    }

    @Override
    public boolean getMoreResults(int current) throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            if (this.results == null) {
                return false;
            }

            boolean streamingMode = meetsConditionsForStreamingResultSet();
            if (streamingMode && this.results.hasRows()) {
                while (this.results.next()) {
                    // need to drain remaining rows to get to server status which tells us whether more results actually exist or not
                }
            }

            ResultSetInternalMethods nextResultSet = (ResultSetInternalMethods) this.results.getNextResultset();

            switch (current) {
                case java.sql.Statement.CLOSE_CURRENT_RESULT:
                    if (this.results != null) {
                        if (!(streamingMode || this.dontTrackOpenResources.getValue())) {
                            this.results.doClose(CloseOption.IMPLICIT);
                        }
                        if (this.results != null) {
                            this.results.clearNextResultset();
                        }
                    }
                    break;

                case java.sql.Statement.CLOSE_ALL_RESULTS:
                    if (this.results != null) {
                        if (!(streamingMode || this.dontTrackOpenResources.getValue())) {
                            this.results.doClose(CloseOption.IMPLICIT);
                        }
                        if (this.results != null) {
                            this.results.clearNextResultset();
                        }
                    }
                    closeAllOpenResults();
                    break;

                case java.sql.Statement.KEEP_CURRENT_RESULT:
                    if (!this.dontTrackOpenResources.getValue()) {
                        this.openResultSets.add(this.results);
                    }
                    this.results.clearNextResultset();
                    break;

                default:
                    throw SQLError.createSQLException(Messages.getString("Statement.19"), MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT,
                            getExceptionInterceptor());
            }

            this.results = nextResultSet;

            if (this.results == null) {
                this.updateCount = -1;
                this.lastInsertId = -1;
            } else if (this.results.hasRows()) {
                this.updateCount = -1;
                this.lastInsertId = -1;
            } else {
                this.updateCount = this.results.getUpdateCount();
                this.lastInsertId = this.results.getUpdateID();
            }

            boolean moreResults = this.results != null && this.results.hasRows();
            if (!moreResults) {
                checkAndPerformCloseOnCompletionAction();
            }
            return moreResults;
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public int getQueryTimeout() throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            return Math.toIntExact(getTimeoutInMillis() / 1000);
        } finally {
            connectionLock.unlock();
        }
    }

    /**
     * Parses actual record count from 'info' message
     *
     * @param serverInfo
     *            server info message
     * @return records count
     */
    private long getRecordCountFromInfo(String serverInfo) {
        StringBuilder recordsBuf = new StringBuilder();
        long recordsCount = 0;
        long duplicatesCount = 0;

        char c = (char) 0;

        int length = serverInfo.length();
        int i = 0;

        for (; i < length; i++) {
            c = serverInfo.charAt(i);

            if (Character.isDigit(c)) {
                break;
            }
        }

        recordsBuf.append(c);
        i++;

        for (; i < length; i++) {
            c = serverInfo.charAt(i);

            if (!Character.isDigit(c)) {
                break;
            }

            recordsBuf.append(c);
        }

        recordsCount = Long.parseLong(recordsBuf.toString());

        StringBuilder duplicatesBuf = new StringBuilder();

        for (; i < length; i++) {
            c = serverInfo.charAt(i);

            if (Character.isDigit(c)) {
                break;
            }
        }

        duplicatesBuf.append(c);
        i++;

        for (; i < length; i++) {
            c = serverInfo.charAt(i);

            if (!Character.isDigit(c)) {
                break;
            }

            duplicatesBuf.append(c);
        }

        duplicatesCount = Long.parseLong(duplicatesBuf.toString());

        return recordsCount - duplicatesCount;
    }

    @Override
    public java.sql.ResultSet getResultSet() throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            return this.results != null && this.results.hasRows() ? (java.sql.ResultSet) this.results : null;
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public int getResultSetConcurrency() throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            return this.resultSetConcurrency;
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public int getResultSetHoldability() throws SQLException {
        return java.sql.ResultSet.HOLD_CURSORS_OVER_COMMIT;
    }

    @Override
    public ResultSetInternalMethods getResultSetInternal() {
        try {
            Lock connectionLock = checkClosed().getConnectionLock();
            connectionLock.lock();
            try {
                return this.results;
            } finally {
                connectionLock.unlock();
            }
        } catch (StatementIsClosedException e) {
            return this.results; // you end up with the same thing as before, you'll get exception when actually trying to use it
        }
    }

    @Override
    public int getResultSetType() throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            return this.query.getResultType().getIntValue();
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public int getUpdateCount() throws SQLException {
        return Util.truncateAndConvertToInt(getLargeUpdateCount());
    }

    @Override
    public java.sql.SQLWarning getWarnings() throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {

            if (isClearWarningsCalled()) {
                return null;
            }

            SQLWarning pendingWarningsFromServer = this.session.getProtocol().convertShowWarningsToSQLWarnings(false);

            if (this.warningChain != null) {
                this.warningChain.setNextWarning(pendingWarningsFromServer);
            } else {
                this.warningChain = pendingWarningsFromServer;
            }

            return this.warningChain;
        } finally {
            connectionLock.unlock();
        }
    }

    /**
     * Close this Statement and release resources. By default the close is considered explicit and does not propagate to dependents.
     */
    @Override
    public void doClose(CloseOption... options) throws SQLException {
        JdbcConnection locallyScopedConn = this.connection;

        if (locallyScopedConn == null || this.isClosed) {
            return; // already closed
        }

        // do it ASAP to reduce the chance of calling this method concurrently from ConnectionImpl.closeAllOpenStatements()
        if (!this.dontTrackOpenResources.getValue()) {
            locallyScopedConn.unregisterStatement(this);
        }

        if (this.useUsageAdvisor) {
            if (CloseOption.IMPLICIT.in(options)) {
                this.session.getProfilerEventHandler().processEvent(ProfilerEvent.TYPE_USAGE, this.session, this, null, 0, new Throwable(),
                        Messages.getString("Statement.63"));
            }
        }

        if (CloseOption.PROPAGATE.in(options) && !this.holdResultsOpenOverClose && !this.dontTrackOpenResources.getValue()) {
            if (this.results != null) {
                try {
                    this.results.close();
                } catch (Exception ex) {
                }
            }
            if (this.generatedKeysResults != null) {
                try {
                    this.generatedKeysResults.close();
                } catch (Exception ex) {
                }
            }
            closeAllOpenResults();
        }

        clearAttributes();

        this.isClosed = true;

        closeQuery();

        this.results = null;
        this.generatedKeysResults = null;
        this.connection = null;
        this.session = null;
        this.warningChain = null;
        this.openResultSets = null;
        this.batchedGeneratedKeys = null;
        this.pingTarget = null;
        this.resultSetFactory = null;
    }

    @Override
    public void setCursorName(String name) throws SQLException {
        // No-op
    }

    @Override
    public void setEscapeProcessing(boolean enable) throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            this.doEscapeProcessing = enable;
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public void setFetchDirection(int direction) throws SQLException {
        switch (direction) {
            case java.sql.ResultSet.FETCH_FORWARD:
            case java.sql.ResultSet.FETCH_REVERSE:
            case java.sql.ResultSet.FETCH_UNKNOWN:
                break;

            default:
                throw SQLError.createSQLException(Messages.getString("Statement.5"), MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT,
                        getExceptionInterceptor());
        }
    }

    @Override
    public void setFetchSize(int rows) throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            if (rows < 0 && rows != Integer.MIN_VALUE) {
                throw SQLError.createSQLException(Messages.getString("Statement.7"), MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT,
                        getExceptionInterceptor());
            }

            this.query.setResultFetchSize(rows);
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public void setHoldResultsOpenOverClose(boolean holdResultsOpenOverClose) {
        try {
            Lock connectionLock = checkClosed().getConnectionLock();
            connectionLock.lock();
            try {
                this.holdResultsOpenOverClose = holdResultsOpenOverClose;
            } finally {
                connectionLock.unlock();
            }
        } catch (StatementIsClosedException e) {
            // FIXME: can't break interface at this point
        }
    }

    @Override
    public void setMaxFieldSize(int max) throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            if (max < 0) {
                throw SQLError.createSQLException(Messages.getString("Statement.11"), MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT,
                        getExceptionInterceptor());
            }

            int maxBuf = this.maxAllowedPacket.getValue();

            if (max > maxBuf) {
                throw SQLError.createSQLException(Messages.getString("Statement.13", new Object[] { Long.valueOf(maxBuf) }),
                        MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT, getExceptionInterceptor());
            }

            this.maxFieldSize = max;
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public void setMaxRows(int max) throws SQLException {
        setLargeMaxRows(max);
    }

    @Override
    public void setQueryTimeout(int seconds) throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            if (seconds < 0) {
                throw SQLError.createSQLException(Messages.getString("Statement.21"), MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT,
                        getExceptionInterceptor());
            }

            setTimeoutInMillis(seconds * 1000);
        } finally {
            connectionLock.unlock();
        }
    }

    /**
     * Sets the concurrency for result sets generated by this statement
     *
     * @param concurrencyFlag
     *            concurrency flag
     * @throws SQLException
     *             if a database access error occurs or this method is called on a closed PreparedStatement
     */
    void setResultSetConcurrency(int concurrencyFlag) throws SQLException {
        try {
            Lock connectionLock = checkClosed().getConnectionLock();
            connectionLock.lock();
            try {
                this.resultSetConcurrency = concurrencyFlag;
                // updating resultset factory because concurrency is cached there
                this.resultSetFactory = new ResultSetFactory(this.connection, this);
            } finally {
                connectionLock.unlock();
            }
        } catch (StatementIsClosedException e) {
            // FIXME: Can't break interface atm, we'll get the exception later when you try and do something useful with a closed statement...
        }
    }

    /**
     * Sets the result set type for result sets generated by this statement
     *
     * @param typeFlag
     *            {@link com.mysql.cj.protocol.Resultset.Type}
     * @throws SQLException
     *             if a database access error occurs or this method is called on a closed PreparedStatement
     */
    void setResultSetType(Resultset.Type typeFlag) throws SQLException {
        try {
            Lock connectionLock = checkClosed().getConnectionLock();
            connectionLock.lock();
            try {
                this.query.setResultType(typeFlag);
                // updating resultset factory because type is cached there
                this.resultSetFactory = new ResultSetFactory(this.connection, this);
            } finally {
                connectionLock.unlock();
            }
        } catch (StatementIsClosedException e) {
            // FIXME: Can't break interface atm, we'll get the exception later when you try and do something useful with a closed statement...
        }
    }

    void setResultSetType(int typeFlag) throws SQLException {
        this.query.setResultType(Type.fromValue(typeFlag, Type.FORWARD_ONLY));
    }

    protected void getBatchedGeneratedKeys(java.sql.Statement batchedStatement) throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            if (this.retrieveGeneratedKeys) {
                java.sql.ResultSet rs = null;

                try {
                    rs = batchedStatement.getGeneratedKeys();

                    while (rs.next()) {
                        this.batchedGeneratedKeys.add(new ByteArrayRow(new byte[][] { rs.getBytes(1) }, getExceptionInterceptor()));
                    }
                } finally {
                    if (rs != null) {
                        rs.close();
                    }
                }
            }
        } finally {
            connectionLock.unlock();
        }
    }

    protected void getBatchedGeneratedKeys(int maxKeys) throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            if (this.retrieveGeneratedKeys) {
                ResultSetInternalMethods rs = null;

                try {
                    rs = maxKeys == 0 ? getGeneratedKeysInternal() : getGeneratedKeysInternal(maxKeys);
                    while (rs.next()) {
                        this.batchedGeneratedKeys.add(new ByteArrayRow(new byte[][] { rs.getBytes(1) }, getExceptionInterceptor()));
                    }
                } finally {
                    try {
                        if (rs != null) {
                            rs.doClose();
                        }
                    } finally {
                    }
                }
            }
        } finally {
            connectionLock.unlock();
        }
    }

    private boolean useServerFetch() throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            return this.session.getPropertySet().getBooleanProperty(PropertyKey.useCursorFetch).getValue() && this.query.getResultFetchSize() > 0
                    && this.query.getResultType() == Type.FORWARD_ONLY;
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public boolean isClosed() throws SQLException {
        JdbcConnection locallyScopedConn = this.connection;
        if (locallyScopedConn == null) {
            return true;
        }
        Lock connectionLock = locallyScopedConn.getConnectionLock();
        connectionLock.lock();
        try {
            return this.isClosed;
        } finally {
            connectionLock.unlock();
        }
    }

    private boolean isPoolable = false;

    @Override
    public boolean isPoolable() throws SQLException {
        checkClosed();
        return this.isPoolable;
    }

    @Override
    public void setPoolable(boolean poolable) throws SQLException {
        checkClosed();
        this.isPoolable = poolable;
    }

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

        // This works for classes that aren't actually wrapping anything
        return iface.isInstance(this);
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        try {
            // This works for classes that aren't actually wrapping anything
            return iface.cast(this);
        } catch (ClassCastException cce) {
            throw SQLError.createSQLException(Messages.getString("Common.UnableToUnwrap", new Object[] { iface.toString() }),
                    MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT, getExceptionInterceptor());
        }
    }

    @Override
    public InputStream getLocalInfileInputStream() {
        return this.session.getLocalInfileInputStream();
    }

    @Override
    public void setLocalInfileInputStream(InputStream stream) {
        this.session.setLocalInfileInputStream(stream);
    }

    @Override
    public void setPingTarget(PingTarget pingTarget) {
        this.pingTarget = pingTarget;
    }

    @Override
    public ExceptionInterceptor getExceptionInterceptor() {
        return this.exceptionInterceptor;
    }

    protected boolean containsOnDuplicateKeyInString(String sql) {
        return (!this.dontCheckOnDuplicateKeyUpdateInSQL || this.rewriteBatchedStatements.getValue())
                && QueryInfo.containsOnDuplicateKeyUpdateClause(sql, this.session.getServerSession().isNoBackslashEscapesSet());
    }

    private boolean closeOnCompletion = false;

    @Override
    public void closeOnCompletion() throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            this.closeOnCompletion = true;
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public boolean isCloseOnCompletion() throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            return this.closeOnCompletion;
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public long[] executeLargeBatch() throws SQLException {
        return executeBatchInternal();
    }

    @Override
    public long executeLargeUpdate(String sql) throws SQLException {
        return executeUpdateInternal(sql, false, false);
    }

    @Override
    public long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
        return executeUpdateInternal(sql, false, autoGeneratedKeys == java.sql.Statement.RETURN_GENERATED_KEYS);
    }

    @Override
    public long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLException {
        return executeUpdateInternal(sql, false, columnIndexes != null && columnIndexes.length > 0);
    }

    @Override
    public long executeLargeUpdate(String sql, String[] columnNames) throws SQLException {
        return executeUpdateInternal(sql, false, columnNames != null && columnNames.length > 0);
    }

    @Override
    public long getLargeMaxRows() throws SQLException {
        // Max rows is limited by MySQLDefs.MAX_ROWS anyway...
        return getMaxRows();
    }

    @Override
    public long getLargeUpdateCount() throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            if (this.results != null) {
                return this.results.hasRows() ? -1 : this.results.getUpdateCount();
            }

            return this.updateCount;
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public void setLargeMaxRows(long max) throws SQLException {
        Lock connectionLock = checkClosed().getConnectionLock();
        connectionLock.lock();
        try {
            if (max > MAX_ROWS || max < 0) {
                throw SQLError.createSQLException(Messages.getString("Statement.15") + " " + max + (max > MAX_ROWS ? " > " + MAX_ROWS : " < 0") + ".",
                        MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT, getExceptionInterceptor());
            }

            if (max == 0) {
                max = -1;
            }

            this.maxRows = (int) max;
        } finally {
            connectionLock.unlock();
        }
    }

    @Override
    public String getCurrentDatabase() {
        return this.query.getCurrentDatabase();
    }

    public long getServerStatementId() {
        throw ExceptionFactory.createException(CJOperationNotSupportedException.class, Messages.getString("Statement.65"));
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T extends Resultset, M extends Message> ProtocolEntityFactory<T, M> getResultSetFactory() {
        return (ProtocolEntityFactory<T, M>) this.resultSetFactory;
    }

    @Override
    public int getId() {
        return this.query.getId();
    }

    @Override
    public void setCancelStatus(CancelStatus cs) {
        this.query.setCancelStatus(cs);
    }

    @Override
    public void checkCancelTimeout() {
        this.query.checkCancelTimeout();
    }

    @Override
    public Session getSession() {
        return this.session;
    }

    @Override
    public Lock getCancelTimeoutLock() {
        return this.query.getCancelTimeoutLock();
    }

    @Override
    public void closeQuery() {
        if (this.query != null) {
            this.query.closeQuery();
        }
    }

    @Override
    public int getResultFetchSize() {
        return this.query.getResultFetchSize();
    }

    @Override
    public void setResultFetchSize(int fetchSize) {
        this.query.setResultFetchSize(fetchSize);
    }

    @Override
    public Resultset.Type getResultType() {
        return this.query.getResultType();
    }

    @Override
    public void setResultType(Resultset.Type resultSetType) {
        this.query.setResultType(resultSetType);
    }

    @Override
    public long getTimeoutInMillis() {
        return this.query.getTimeoutInMillis();
    }

    @Override
    public void setTimeoutInMillis(long timeoutInMillis) {
        this.query.setTimeoutInMillis(timeoutInMillis);
    }

    @Override
    public long getExecuteTime() {
        return this.query.getExecuteTime();
    }

    @Override
    public void setExecuteTime(long executeTime) {
        this.query.setExecuteTime(executeTime);
    }

    @Override
    public AtomicBoolean getStatementExecuting() {
        return this.query.getStatementExecuting();
    }

    @Override
    public void setCurrentDatabase(String currentDb) {
        this.query.setCurrentDatabase(currentDb);
    }

    @Override
    public boolean isClearWarningsCalled() {
        return this.query.isClearWarningsCalled();
    }

    @Override
    public void setClearWarningsCalled(boolean clearWarningsCalled) {
        this.query.setClearWarningsCalled(clearWarningsCalled);
    }

    @Override
    public Query getQuery() {
        return this.query;
    }

    @Override
    public QueryAttributesBindings getQueryAttributesBindings() {
        return this.query.getQueryAttributesBindings();
    }

    @Override
    public void setAttribute(String name, Object value) {
        getQueryAttributesBindings().setAttribute(name, value);
    }

    @Override
    public void clearAttributes() {
        QueryAttributesBindings qab = getQueryAttributesBindings();
        if (qab != null) {
            qab.clearAttributes();
        }
    }

    /**
     * Determines whether the supplied identifier is "simple", meaning it can be used in SQL without quoting under the rules implemented by
     * {@link StringUtils#isSimpleIdentifier(String, java.util.function.Predicate)}.
     * <p>
     * This is typically used as a fast-path to avoid quoting identifiers that consist only of allowed characters and do not require delimiter quoting for
     * correctness.
     *
     * @param identifier
     *            The identifier text to test (for example a column or table name).
     * @return {@code true} if the identifier is considered simple and can be used unquoted; {@code false} otherwise.
     * @throws SQLException
     *             Declared for interface/override compatibility; this implementation does not throw based on the current logic but callers should still handle
     *             it per the contract.
     *
     * @see StringUtils#isSimpleIdentifier(String, java.util.function.Predicate)
     */
    @Override
    public boolean isSimpleIdentifier(String identifier) throws SQLException {
        return this.connection.isSimpleIdentifier(identifier);
    }

    /**
     * Returns an SQL identifier rendered for inclusion in a statement, applying quoting rules based on the current session configuration.
     * <p>
     * The identifier quote character is derived from the session/server configuration (for example {@code `} or {@code "} depending on SQL mode such as
     * ANSI_QUOTES).
     * <p>
     * Fast-path behavior for "simple" identifiers:
     * <ul>
     * <li>If {@code identifier} is considered a simple identifier (per {@code isSimpleIdentifier}), then:
     * <ul>
     * <li>when {@code alwaysDelimit} is {@code true}, the identifier is surrounded with the session's identifier quote character,</li>
     * <li>otherwise it is returned unchanged (unquoted).</li>
     * </ul>
     * </li>
     * </ul>
     * <p>
     * General behavior for non-simple identifiers:
     * <ul>
     * <li>Determines whether ANSI quoted identifiers are enabled (ANSI_QUOTES) and whether backslash escapes are permitted (NO_BACKSLASH_ESCAPES).</li>
     * <li>Delegates to {@link StringUtils#enquoteIdentifier(String, char, boolean, boolean, boolean)} to validate and/or quote the identifier accordingly.</li>
     * </ul>
     * <p>
     * Note: This method is intended for rendering identifiers (for example schema/table/column names). It does not validate object existence and does not
     * perform SQL parsing; callers should ensure the provided identifier is trusted or otherwise handled safely.
     *
     * @param identifier
     *            The identifier text to render (for example a table or column name).
     * @param alwaysDelimit
     *            If {@code true}, always quote simple identifiers; if {@code false}, simple identifiers are returned unquoted
     * @return The rendered identifier, quoted as needed for the current session's identifier quoting and escaping rules.
     * @throws SQLException
     *             If an error occurs.
     *
     * @see StringUtils#quoteIdentifier(String, boolean)
     */
    @Override
    public String enquoteIdentifier(String identifier, boolean alwaysDelimit) throws SQLException {
        return this.connection.enquoteIdentifier(identifier, alwaysDelimit);
    }

    /**
     * Returns an SQL string literal representation of the supplied value, rendered for inclusion in a statement and using quoting/escaping rules derived from
     * the current session configuration.
     * <p>
     * Behavior:
     * <ul>
     * <li>If {@code val} is {@code null}, returns the SQL {@code NULL} keyword (unquoted).</li>
     * <li>Otherwise determines session SQL-mode settings that influence quoting/escaping:
     * <ul>
     * <li>ANSI_QUOTES: affects whether {@code "} may be treated as a string delimiter vs. identifier delimiter, depending on the quoting rules.</li>
     * <li>NO_BACKSLASH_ESCAPES: controls whether backslash escaping is recognized when validating/producing a literal.</li>
     * </ul>
     * </li>
     * <li>Delegates to {@link StringUtils#enquoteLiteral(String, boolean, boolean, boolean)} (or equivalent) to perform the actual quoting and escaping,
     * honoring
     * the session's pedantic mode setting.</li>
     * </ul>
     *
     * @param val
     *            The value to render as an SQL string literal; {@code null} is rendered as {@code NULL}.
     * @return An SQL literal suitable for embedding in a statement (e.g., {@code 'text'}), or {@code NULL} when {@code val} is {@code null}.
     * @throws SQLException
     *             Declared for interface/override compatibility and to allow callers to handle session-related failures consistently.
     *
     * @see StringUtils#enquoteLiteral(String, boolean, boolean, boolean)
     */
    @Override
    public String enquoteLiteral(String val) throws SQLException {
        return this.connection.enquoteLiteral(val);
    }

    /**
     * Returns an SQL national character set string literal (NCHAR literal) representation of the supplied value, rendered for inclusion in a statement.
     * <p>
     * Behavior:
     * <ul>
     * <li>If {@code val} is {@code null}, returns the SQL {@code NULL} keyword (unquoted).</li>
     * <li>Determines whether backslash escapes are enabled based on the current session's SQL_MODE containing {@code NO_BACKSLASH_ESCAPES} setting.</li>
     * <li>In non-pedantic mode, if {@code val} already appears to be prefixed as an NCHAR literal (i.e., {@code N'...'} or {@code n'...'}), the method:
     * <ul>
     * <li>extracts the quoted literal portion (starting at the {@code '}),</li>
     * <li>delegates to {@link StringUtils#enquoteLiteral(String, boolean, boolean, boolean)} to validate/fix the quoting/escaping of that portion, and</li>
     * <li>returns the original value unchanged if it was already properly quoted.</li>
     * </ul>
     * </li>
     * <li>Otherwise, treats {@code val} as raw content, quotes it as a standard single-quoted literal, and prefixes the result with {@code N}.</li>
     * </ul>
     * <p>
     * Notes:
     * <ul>
     * <li>Double quotes are not considered valid delimiters for NCHAR literals; this method only accepts/produces the {@code N'...'} form.</li>
     * <li>This method forces the literal-quoting helper to treat {@code "} as <em>not</em> a string delimiter (passes {@code ansiQuotes=true} to the helper),
     * because the national character literal syntax is always single-quoted.</li>
     * </ul>
     *
     * @param val
     *            The value to render as an SQL national character set string literal; {@code null} is rendered as {@code NULL}. The input may be raw content or
     *            may already be in {@code N'...'} / {@code n'...'} form.
     * @return A national character set literal suitable for embedding in SQL (e.g., {@code N'text'}), or {@code NULL} when {@code val} is {@code null}.
     * @throws SQLException
     *             Declared for interface/override compatibility and to allow callers to handle session-related failures consistently.
     */
    @Override
    public String enquoteNCharLiteral(String val) throws SQLException {
        return this.connection.enquoteNCharLiteral(val);
    }

}
