/*
  Copyright (c) 2002, 2019, Oracle and/or its affiliates. All rights reserved.

  The MySQL Connector/J is licensed under the terms of the GPLv2
  <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most MySQL Connectors.
  There are special exceptions to the terms and conditions of the GPLv2 as it is applied to
  this software, see the FOSS License Exception
  <http://www.mysql.com/about/legal/licensing/foss-exception.html>.

  This program is free software; you can redistribute it and/or modify it under the terms
  of the GNU General Public License as published by the Free Software Foundation; version 2
  of the License.

  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 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.jdbc;

import java.io.InputStream;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.math.BigInteger;
import java.sql.BatchUpdateException;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Enumeration;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicBoolean;

import com.mysql.jdbc.exceptions.MySQLStatementCancelledException;
import com.mysql.jdbc.exceptions.MySQLTimeoutException;
import com.mysql.jdbc.profiler.ProfilerEvent;

/**
 * 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 Statement {
    protected static final String PING_MARKER = "/* ping */";

    protected static final String[] ON_DUPLICATE_KEY_UPDATE_CLAUSE = new String[] { "ON", "DUPLICATE", "KEY", "UPDATE" };

    /**
     * Thread used to implement query timeouts...Eventually we could be more
     * efficient and have one thread with timers, but this is a straightforward
     * and simple way to implement a feature that isn't used all that often.
     */
    class CancelTask extends TimerTask {
        SQLException caughtWhileCancelling = null;
        StatementImpl toCancel;
        Properties origConnProps = null;
        String origConnURL = "";
        long origConnId = 0;

        CancelTask(StatementImpl cancellee) throws SQLException {
            this.toCancel = cancellee;
            this.origConnProps = new Properties();

            Properties props = StatementImpl.this.connection.getProperties();

            Enumeration<?> keys = props.propertyNames();

            while (keys.hasMoreElements()) {
                String key = keys.nextElement().toString();
                this.origConnProps.setProperty(key, props.getProperty(key));
            }

            this.origConnURL = StatementImpl.this.connection.getURL();
            this.origConnId = StatementImpl.this.connection.getId();
        }

        @Override
        public void run() {

            Thread cancelThread = new Thread() {

                @Override
                public void run() {

                    Connection cancelConn = null;
                    java.sql.Statement cancelStmt = null;

                    try {
                        MySQLConnection physicalConn = StatementImpl.this.physicalConnection.get();
                        if (physicalConn != null) {
                            if (physicalConn.getQueryTimeoutKillsConnection()) {
                                CancelTask.this.toCancel.wasCancelled = true;
                                CancelTask.this.toCancel.wasCancelledByTimeout = true;
                                physicalConn.realClose(false, false, true,
                                        new MySQLStatementCancelledException(Messages.getString("Statement.ConnectionKilledDueToTimeout")));
                            } else {
                                synchronized (StatementImpl.this.cancelTimeoutMutex) {
                                    if (CancelTask.this.origConnURL.equals(physicalConn.getURL())) {
                                        // All's fine
                                        cancelConn = physicalConn.duplicate();
                                        cancelStmt = cancelConn.createStatement();
                                        cancelStmt.execute("KILL QUERY " + physicalConn.getId());
                                    } else {
                                        try {
                                            cancelConn = (Connection) DriverManager.getConnection(CancelTask.this.origConnURL, CancelTask.this.origConnProps);
                                            cancelStmt = cancelConn.createStatement();
                                            cancelStmt.execute("KILL QUERY " + CancelTask.this.origConnId);
                                        } catch (NullPointerException npe) {
                                            // Log this? "Failed to connect to " + origConnURL + " and KILL query"
                                        }
                                    }
                                    CancelTask.this.toCancel.wasCancelled = true;
                                    CancelTask.this.toCancel.wasCancelledByTimeout = true;
                                }
                            }
                        }
                    } catch (SQLException sqlEx) {
                        CancelTask.this.caughtWhileCancelling = sqlEx;
                    } catch (NullPointerException npe) {
                        // Case when connection closed while starting to cancel.
                        // We can't easily synchronize this, because then one thread can't cancel() a running query.
                        // Ignore, we shouldn't re-throw this, because the connection's already closed, so the statement has been timed out.
                    } finally {
                        if (cancelStmt != null) {
                            try {
                                cancelStmt.close();
                            } catch (SQLException sqlEx) {
                                throw new RuntimeException(sqlEx.toString());
                            }
                        }

                        if (cancelConn != null) {
                            try {
                                cancelConn.close();
                            } catch (SQLException sqlEx) {
                                throw new RuntimeException(sqlEx.toString());
                            }
                        }

                        CancelTask.this.toCancel = null;
                        CancelTask.this.origConnProps = null;
                        CancelTask.this.origConnURL = null;
                    }
                }
            };

            cancelThread.start();
        }
    }

    /**
     * Mutex to prevent race between returning query results and noticing
     * that we're timed-out or cancelled.
     */

    protected Object cancelTimeoutMutex = new Object();

    /** Used to generate IDs when profiling. */
    static int statementCounter = 1;

    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 boolean wasCancelled = false;
    protected boolean wasCancelledByTimeout = false;

    /** Holds batched commands */
    protected List<Object> batchedArgs;

    /** The character converter to use (if available) */
    protected SingleByteCharsetConverter charConverter = null;

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

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

    /** The physical connection used to effectively execute the statement */
    protected Reference<MySQLConnection> physicalConnection = null;

    /** The catalog in use */
    protected String currentCatalog = null;

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

    /** The number of rows to fetch at a time (currently ignored) */
    private int fetchSize = 0;

    /** 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 = MysqlIO.getMaxBuf();

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

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

    /** 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 type of this result set (scroll sensitive or in-sensitive) */
    protected int resultSetType = 0;

    /** Used to identify this statement when profiling. */
    protected int statementId;

    /** The timeout for a query */
    protected int timeoutInMillis = 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;

    /** Has clearWarnings() been called? */
    protected boolean clearWarningsCalled = false;

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

    protected ArrayList<ResultSetRow> batchedGeneratedKeys = null;

    protected boolean retrieveGeneratedKeys = false;

    protected boolean continueBatchOnError = false;

    protected PingTarget pingTarget = null;

    protected boolean useLegacyDatetimeCode;

    protected boolean sendFractionalSeconds;

    private ExceptionInterceptor exceptionInterceptor;

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

    /** Are we currently executing a statement? */
    protected final AtomicBoolean statementExecuting = new AtomicBoolean(false);

    /** Are we currently closing results implicitly (internally)? */
    private boolean isImplicitlyClosingResults = false;

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

        this.connection = c;
        this.exceptionInterceptor = this.connection.getExceptionInterceptor();

        this.currentCatalog = catalog;
        this.pedantic = this.connection.getPedantic();
        this.continueBatchOnError = this.connection.getContinueBatchOnError();
        this.useLegacyDatetimeCode = this.connection.getUseLegacyDatetimeCode();
        this.sendFractionalSeconds = this.connection.getSendFractionalSeconds();
        this.doEscapeProcessing = this.connection.getEnableEscapeProcessing();

        if (!this.connection.getDontTrackOpenResources()) {
            this.connection.registerStatement(this);
        }

        this.maxFieldSize = this.connection.getMaxAllowedPacket();

        int defaultFetchSize = this.connection.getDefaultFetchSize();
        if (defaultFetchSize != 0) {
            setFetchSize(defaultFetchSize);
        }

        if (this.connection.getUseUnicode()) {
            this.charEncoding = this.connection.getEncoding();
            this.charConverter = this.connection.getCharsetConverter(this.charEncoding);
        }

        boolean profiling = this.connection.getProfileSql() || this.connection.getUseUsageAdvisor() || this.connection.getLogSlowQueries();
        if (this.connection.getAutoGenerateTestcaseScript() || profiling) {
            this.statementId = statementCounter++;
        }
        if (profiling) {
            this.profileSQL = this.connection.getProfileSql();
            this.useUsageAdvisor = this.connection.getUseUsageAdvisor();
        }

        int maxRowsConn = this.connection.getMaxRows();
        if (maxRowsConn != -1) {
            setMaxRows(maxRowsConn);
        }

        this.holdResultsOpenOverClose = this.connection.getHoldResultsOpenOverStatementClose();

        this.version5013OrNewer = this.connection.versionMeetsMinimum(5, 0, 13);
    }

    /**
     * @param sql
     * 
     * @throws SQLException
     */
    public void addBatch(String sql) throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            if (this.batchedArgs == null) {
                this.batchedArgs = new ArrayList<Object>();
            }

            if (sql != null) {
                this.batchedArgs.add(sql);
            }
        }
    }

    /**
     * Get the batched args as added by the addBatch method(s).
     * The list is unmodifiable and might contain any combination of String,
     * BatchParams, or BatchedBindValues depending on how the parameters were
     * batched.
     * 
     * @return an unmodifiable List of batched args
     */
    public List<Object> getBatchedArgs() {
        return this.batchedArgs == null ? null : Collections.unmodifiableList(this.batchedArgs);
    }

    /**
     * Cancels this Statement object if both the DBMS and driver support
     * aborting an SQL statement. This method can be used by one thread to
     * cancel a statement that is being executed by another thread.
     */
    public void cancel() throws SQLException {
        if (!this.statementExecuting.get()) {
            return;
        }

        if (!this.isClosed && this.connection != null && this.connection.versionMeetsMinimum(5, 0, 0)) {
            Connection cancelConn = null;
            java.sql.Statement cancelStmt = null;

            try {
                cancelConn = this.connection.duplicate();
                cancelStmt = cancelConn.createStatement();
                cancelStmt.execute("KILL QUERY " + this.connection.getIO().getThreadId());
                this.wasCancelled = true;
            } finally {
                if (cancelStmt != null) {
                    cancelStmt.close();
                }

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

        }
    }

    // --------------------------JDBC 2.0-----------------------------

    /**
     * Checks if closed() has been called, and throws an exception if so
     * 
     * @throws SQLException
     *             if this statement has been closed
     */
    protected MySQLConnection checkClosed() throws SQLException {
        MySQLConnection c = this.connection;

        if (c == null) {
            throw SQLError.createSQLException(Messages.getString("Statement.49"), SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
        }

        return c;
    }

    /**
     * Checks if the given SQL query with the given first non-ws char is a DML
     * statement. Throws an exception if it is.
     * 
     * @param sql
     *            the SQL to check
     * @param firstStatementChar
     *            the UC first non-ws char of the statement
     * 
     * @throws SQLException
     *             if the statement contains DML
     */
    protected void checkForDml(String sql, char firstStatementChar) throws SQLException {
        if ((firstStatementChar == 'I') || (firstStatementChar == 'U') || (firstStatementChar == 'D') || (firstStatementChar == 'A')
                || (firstStatementChar == 'C') || (firstStatementChar == 'T') || (firstStatementChar == 'R')) {
            String noCommentSql = StringUtils.stripComments(sql, "'\"", "'\"", true, false, true, true);

            if (StringUtils.startsWithIgnoreCaseAndWs(noCommentSql, "INSERT") || StringUtils.startsWithIgnoreCaseAndWs(noCommentSql, "UPDATE")
                    || StringUtils.startsWithIgnoreCaseAndWs(noCommentSql, "DELETE") || StringUtils.startsWithIgnoreCaseAndWs(noCommentSql, "DROP")
                    || StringUtils.startsWithIgnoreCaseAndWs(noCommentSql, "CREATE") || StringUtils.startsWithIgnoreCaseAndWs(noCommentSql, "ALTER")
                    || StringUtils.startsWithIgnoreCaseAndWs(noCommentSql, "TRUNCATE") || StringUtils.startsWithIgnoreCaseAndWs(noCommentSql, "RENAME")) {
                throw SQLError.createSQLException(Messages.getString("Statement.57"), SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
            }
        }
    }

    /**
     * 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"), SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
        }

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

    /**
     * JDBC 2.0 Make the set of commands in the current batch empty. This method
     * is optional.
     * 
     * @exception SQLException
     *                if a database-access error occurs, or the driver does not
     *                support batch statements
     */
    public void clearBatch() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            if (this.batchedArgs != null) {
                this.batchedArgs.clear();
            }
        }
    }

    /**
     * After this call, getWarnings returns null until a new warning is reported
     * for this Statement.
     * 
     * @exception SQLException
     *                if a database access error occurs (why?)
     */
    public void clearWarnings() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            this.clearWarningsCalled = true;
            this.warningChain = null;
        }
    }

    /**
     * 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.
     * 
     * <p>
     * <B>Note:</B> A Statement is automatically closed when it is garbage collected. When a Statement is closed, its current ResultSet, if one exists, is also
     * closed.
     * </p>
     * 
     * @exception SQLException
     *                if a database access error occurs
     */
    public void close() throws SQLException {
        realClose(true, true);
    }

    /**
     * Close any open result sets that have been 'held open'
     */
    protected void closeAllOpenResults() throws SQLException {
        MySQLConnection locallyScopedConn = this.connection;

        if (locallyScopedConn == null) {
            return; // already closed
        }

        synchronized (locallyScopedConn.getConnectionMutex()) {
            if (this.openResults != null) {
                for (ResultSetInternalMethods element : this.openResults) {
                    try {
                        element.realClose(false);
                    } catch (SQLException sqlEx) {
                        AssertionFailedException.shouldNotHappen(sqlEx);
                    }
                }

                this.openResults.clear();
            }
        }
    }

    /**
     * Close all result sets in this statement. This includes multi-results
     */
    protected void implicitlyCloseAllOpenResults() throws SQLException {
        this.isImplicitlyClosingResults = true;
        try {
            if (!(this.connection.getHoldResultsOpenOverStatementClose() || this.connection.getDontTrackOpenResources() || this.holdResultsOpenOverClose)) {
                if (this.results != null) {
                    this.results.realClose(false);
                }
                if (this.generatedKeysResults != null) {
                    this.generatedKeysResults.realClose(false);
                }
                closeAllOpenResults();
            }
        } finally {
            this.isImplicitlyClosingResults = false;
        }
    }

    public void removeOpenResultSet(ResultSetInternalMethods rs) {
        try {
            synchronized (checkClosed().getConnectionMutex()) {
                if (this.openResults != null) {
                    this.openResults.remove(rs);
                }

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

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

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

    public int getOpenResultSetCount() {
        try {
            synchronized (checkClosed().getConnectionMutex()) {
                if (this.openResults != null) {
                    return this.openResults.size();
                }

                return 0;
            }
        } catch (SQLException 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 {
            synchronized (checkClosed().getConnectionMutex()) {
                if (isCloseOnCompletion() && !this.connection.getDontTrackOpenResources() && getOpenResultSetCount() == 0
                        && (this.results == null || !this.results.reallyResult() || this.results.isClosed())
                        && (this.generatedKeysResults == null || !this.generatedKeysResults.reallyResult() || this.generatedKeysResults.isClosed())) {
                    realClose(false, false);
                }
            }
        } catch (SQLException e) {
        }
    }

    /**
     * @param sql
     */
    private ResultSetInternalMethods createResultSetUsingServerFetch(String sql) throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            java.sql.PreparedStatement pStmt = this.connection.prepareStatement(sql, this.resultSetType, this.resultSetConcurrency);

            pStmt.setFetchSize(this.fetchSize);

            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 = ((StatementImpl) pStmt).getResultSetInternal();

            rs.setStatementUsedForFetchingRows((com.mysql.jdbc.PreparedStatement) pStmt);

            this.results = rs;

            return rs;
        }
    }

    /**
     * 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 createStreamingResultSet() {
        return ((this.resultSetType == java.sql.ResultSet.TYPE_FORWARD_ONLY) && (this.resultSetConcurrency == java.sql.ResultSet.CONCUR_READ_ONLY)
                && (this.fetchSize == Integer.MIN_VALUE));
    }

    private int originalResultSetType = 0;
    private int originalFetchSize = 0;

    /*
     * (non-Javadoc)
     * 
     * @see com.mysql.jdbc.IStatement#enableStreamingResults()
     */
    public void enableStreamingResults() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            this.originalResultSetType = this.resultSetType;
            this.originalFetchSize = this.fetchSize;

            setFetchSize(Integer.MIN_VALUE);
            setResultSetType(ResultSet.TYPE_FORWARD_ONLY);
        }
    }

    public void disableStreamingResults() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            if (this.fetchSize == Integer.MIN_VALUE && this.resultSetType == ResultSet.TYPE_FORWARD_ONLY) {
                setFetchSize(this.originalFetchSize);
                setResultSetType(this.originalResultSetType);
            }
        }
    }

    /**
     * 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 RowDataDynamic.close().
     */
    protected void setupStreamingTimeout(MySQLConnection con) throws SQLException {
        if (createStreamingResultSet() && con.getNetTimeoutForStreamingResults() > 0) {
            executeSimpleNonQuery(con, "SET net_write_timeout=" + con.getNetTimeoutForStreamingResults());
        }
    }

    /**
     * Execute a SQL statement that may return multiple results. We don't have
     * to worry about this since we do not support multiple ResultSets. You can
     * use getResultSet or getUpdateCount to retrieve the result.
     * 
     * @param sql
     *            any SQL statement
     * 
     * @return true if the next result is a ResulSet, false if it is an update
     *         count or there are no more results
     * 
     * @exception SQLException
     *                if a database access error occurs
     */
    public boolean execute(String sql) throws SQLException {
        return executeInternal(sql, false);
    }

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

        synchronized (locallyScopedConn.getConnectionMutex()) {
            checkClosed();

            checkNullOrEmptyQuery(sql);

            resetCancelledState();

            implicitlyCloseAllOpenResults();

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

                    return true;
                }
            }

            char firstNonWsChar = StringUtils.firstAlphaCharUc(sql, findStartOfStatement(sql));
            boolean maybeSelect = firstNonWsChar == 'S';

            this.retrieveGeneratedKeys = returnGeneratedKeys;

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

            if (!maybeSelect && locallyScopedConn.isReadOnly()) {
                throw SQLError.createSQLException(Messages.getString("Statement.27") + Messages.getString("Statement.28"), SQLError.SQL_STATE_ILLEGAL_ARGUMENT,
                        getExceptionInterceptor());
            }

            boolean readInfoMsgState = locallyScopedConn.isReadInfoMsgEnabled();
            if (returnGeneratedKeys && firstNonWsChar == 'R') {
                // If this is a 'REPLACE' query, we need to be able to parse the 'info' message returned from the server to determine the actual number of keys
                // generated.
                locallyScopedConn.setReadInfoMsgEnabled(true);
            }

            try {
                setupStreamingTimeout(locallyScopedConn);

                if (this.doEscapeProcessing) {
                    Object escapedSqlResult = EscapeProcessor.escapeSQL(sql, locallyScopedConn.serverSupportsConvertFn(), locallyScopedConn);

                    if (escapedSqlResult instanceof String) {
                        sql = (String) escapedSqlResult;
                    } else {
                        sql = ((EscapeProcessorResult) escapedSqlResult).escapedSql;
                    }
                }

                CachedResultSetMetaData cachedMetaData = null;

                ResultSetInternalMethods rs = null;

                this.batchedGeneratedKeys = null;

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

                    String oldCatalog = null;

                    try {
                        if (locallyScopedConn.getEnableQueryTimeouts() && this.timeoutInMillis != 0 && locallyScopedConn.versionMeetsMinimum(5, 0, 0)) {
                            timeoutTask = new CancelTask(this);
                            locallyScopedConn.getCancelTimer().schedule(timeoutTask, this.timeoutInMillis);
                        }

                        if (!locallyScopedConn.getCatalog().equals(this.currentCatalog)) {
                            oldCatalog = locallyScopedConn.getCatalog();
                            locallyScopedConn.setCatalog(this.currentCatalog);
                        }

                        //
                        // Check if we have cached metadata for this query...
                        //

                        Field[] cachedFields = null;

                        if (locallyScopedConn.getCacheResultSetMetadata()) {
                            cachedMetaData = locallyScopedConn.getCachedMetaData(sql);

                            if (cachedMetaData != null) {
                                cachedFields = cachedMetaData.fields;
                            }
                        }

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

                        statementBegins();

                        rs = locallyScopedConn.execSQL(this, sql, this.maxRows, null, this.resultSetType, this.resultSetConcurrency, createStreamingResultSet(),
                                this.currentCatalog, cachedFields);

                        if (timeoutTask != null) {
                            if (timeoutTask.caughtWhileCancelling != null) {
                                throw timeoutTask.caughtWhileCancelling;
                            }

                            timeoutTask.cancel();
                            timeoutTask = null;
                        }

                        synchronized (this.cancelTimeoutMutex) {
                            if (this.wasCancelled) {
                                SQLException cause = null;

                                if (this.wasCancelledByTimeout) {
                                    cause = new MySQLTimeoutException();
                                } else {
                                    cause = new MySQLStatementCancelledException();
                                }

                                resetCancelledState();

                                throw cause;
                            }
                        }
                    } finally {
                        if (timeoutTask != null) {
                            timeoutTask.cancel();
                            locallyScopedConn.getCancelTimer().purge();
                        }

                        if (oldCatalog != null) {
                            locallyScopedConn.setCatalog(oldCatalog);
                        }
                    }
                }

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

                    this.results = rs;

                    rs.setFirstCharOfQuery(firstNonWsChar);

                    if (rs.reallyResult()) {
                        if (cachedMetaData != null) {
                            locallyScopedConn.initializeResultsMetadataFromCache(sql, cachedMetaData, this.results);
                        } else {
                            if (this.connection.getCacheResultSetMetadata()) {
                                locallyScopedConn.initializeResultsMetadataFromCache(sql, null /* will be created */, this.results);
                            }
                        }
                    }
                }

                return ((rs != null) && rs.reallyResult());
            } finally {
                locallyScopedConn.setReadInfoMsgEnabled(readInfoMsgState);

                this.statementExecuting.set(false);
            }
        }
    }

    protected void statementBegins() {
        this.clearWarningsCalled = false;
        this.statementExecuting.set(true);

        MySQLConnection physicalConn = this.connection.getMultiHostSafeProxy().getActiveMySQLConnection();
        while (!(physicalConn instanceof ConnectionImpl)) {
            physicalConn = physicalConn.getMultiHostSafeProxy().getActiveMySQLConnection();
        }
        this.physicalConnection = new WeakReference<MySQLConnection>(physicalConn);
    }

    protected void resetCancelledState() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            if (this.cancelTimeoutMutex == null) {
                return;
            }

            synchronized (this.cancelTimeoutMutex) {
                this.wasCancelled = false;
                this.wasCancelledByTimeout = false;
            }
        }
    }

    /**
     * @see StatementImpl#execute(String, int)
     */
    public boolean execute(String sql, int returnGeneratedKeys) throws SQLException {
        return executeInternal(sql, returnGeneratedKeys == java.sql.Statement.RETURN_GENERATED_KEYS);
    }

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

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

    /**
     * JDBC 2.0 Submit a batch of commands to the database for execution. This
     * method is optional.
     * 
     * @return an array of update counts containing one element for each command
     *         in the batch. The array is ordered according to the order in
     *         which commands were inserted into the batch
     * 
     * @exception SQLException
     *                if a database-access error occurs, or the driver does not
     *                support batch statements
     * @throws java.sql.BatchUpdateException
     */
    public int[] executeBatch() throws SQLException {
        return Util.truncateAndConvertToInt(executeBatchInternal());
    }

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

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

            implicitlyCloseAllOpenResults();

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

            // we timeout the entire batch, not individual statements
            int individualStatementTimeout = this.timeoutInMillis;
            this.timeoutInMillis = 0;

            CancelTask timeoutTask = null;

            try {
                resetCancelledState();

                statementBegins();

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

                    long[] updateCounts = null;

                    if (this.batchedArgs != null) {
                        int nbrCommands = this.batchedArgs.size();

                        this.batchedGeneratedKeys = new ArrayList<ResultSetRow>(this.batchedArgs.size());

                        boolean multiQueriesEnabled = locallyScopedConn.getAllowMultiQueries();

                        if (locallyScopedConn.versionMeetsMinimum(4, 1, 1)
                                && (multiQueriesEnabled || (locallyScopedConn.getRewriteBatchedStatements() && nbrCommands > 4))) {
                            return executeBatchUsingMultiQueries(multiQueriesEnabled, nbrCommands, individualStatementTimeout);
                        }

                        if (locallyScopedConn.getEnableQueryTimeouts() && individualStatementTimeout != 0 && locallyScopedConn.versionMeetsMinimum(5, 0, 0)) {
                            timeoutTask = new CancelTask(this);
                            locallyScopedConn.getCancelTimer().schedule(timeoutTask, 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) this.batchedArgs.get(commandIndex);
                                updateCounts[commandIndex] = executeUpdateInternal(sql, true, true);
                                // 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);
                                    }

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

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

                    if (timeoutTask != null) {
                        if (timeoutTask.caughtWhileCancelling != null) {
                            throw timeoutTask.caughtWhileCancelling;
                        }

                        timeoutTask.cancel();

                        locallyScopedConn.getCancelTimer().purge();
                        timeoutTask = null;
                    }

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

                if (timeoutTask != null) {
                    timeoutTask.cancel();

                    locallyScopedConn.getCancelTimer().purge();
                }

                resetCancelledState();

                this.timeoutInMillis = individualStatementTimeout;

                clearBatch();
            }
        }
    }

    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 !this.version5013OrNewer;
            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.
     * 
     * @return update counts in the same manner as executeBatch()
     * @throws SQLException
     */
    private long[] executeBatchUsingMultiQueries(boolean multiQueriesEnabled, int nbrCommands, int individualStatementTimeout) throws SQLException {

        MySQLConnection locallyScopedConn = checkClosed();

        synchronized (locallyScopedConn.getConnectionMutex()) {
            if (!multiQueriesEnabled) {
                locallyScopedConn.getIO().enableMultiQueries();
            }

            java.sql.Statement batchStmt = null;

            CancelTask timeoutTask = null;

            try {
                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();

                if (locallyScopedConn.getEnableQueryTimeouts() && individualStatementTimeout != 0 && locallyScopedConn.versionMeetsMinimum(5, 0, 0)) {
                    timeoutTask = new CancelTask((StatementImpl) batchStmt);
                    locallyScopedConn.getCancelTimer().schedule(timeoutTask, individualStatementTimeout);
                }

                int counter = 0;

                int numberOfBytesPerChar = 1;

                String connectionEncoding = locallyScopedConn.getEncoding();

                if (StringUtils.startsWithIgnoreCase(connectionEncoding, "utf")) {
                    numberOfBytesPerChar = 3;
                } else if (CharsetMapping.isMultibyteCharset(connectionEncoding)) {
                    numberOfBytesPerChar = 2;
                }

                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;

                for (commandIndex = 0; commandIndex < nbrCommands; commandIndex++) {
                    String nextQuery = (String) this.batchedArgs.get(commandIndex);

                    if (((((queryBuf.length() + nextQuery.length()) * numberOfBytesPerChar) + 1 /* for semicolon */
                            + MysqlIO.HEADER_LENGTH) * escapeAdjust) + 32 > this.connection.getMaxAllowedPacket()) {
                        try {
                            batchStmt.execute(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;
                    }

                    queryBuf.append(nextQuery);
                    queryBuf.append(";");
                    argumentSetsInBatchSoFar++;
                }

                if (queryBuf.length() > 0) {
                    try {
                        batchStmt.execute(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) {
                    if (timeoutTask.caughtWhileCancelling != null) {
                        throw timeoutTask.caughtWhileCancelling;
                    }

                    timeoutTask.cancel();

                    locallyScopedConn.getCancelTimer().purge();

                    timeoutTask = null;
                }

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

                return (updateCounts != null) ? updateCounts : new long[0];
            } finally {
                if (timeoutTask != null) {
                    timeoutTask.cancel();

                    locallyScopedConn.getCancelTimer().purge();
                }

                resetCancelledState();

                try {
                    if (batchStmt != null) {
                        batchStmt.close();
                    }
                } finally {
                    if (!multiQueriesEnabled) {
                        locallyScopedConn.getIO().disableMultiQueries();
                    }
                }
            }
        }
    }

    protected int processMultiCountsAndKeys(StatementImpl batchedStatement, int updateCountCounter, long[] updateCounts) throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            updateCounts[updateCountCounter++] = batchedStatement.getLargeUpdateCount();

            boolean doGenKeys = this.batchedGeneratedKeys != null;

            byte[][] row = null;

            if (doGenKeys) {
                long generatedKey = batchedStatement.getLastInsertID();

                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();

                if (doGenKeys) {
                    long generatedKey = batchedStatement.getLastInsertID();

                    row = new byte[1][];
                    row[0] = StringUtils.getBytes(Long.toString(generatedKey));
                    this.batchedGeneratedKeys.add(new ByteArrayRow(row, getExceptionInterceptor()));
                }
            }

            return updateCountCounter;
        }
    }

    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());
    }

    /**
     * Execute a SQL statement that returns a single ResultSet
     * 
     * @param sql
     *            typically a static SQL SELECT statement
     * 
     * @return a ResulSet that contains the data produced by the query
     * 
     * @exception SQLException
     *                if a database access error occurs
     */
    public java.sql.ResultSet executeQuery(String sql) throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            MySQLConnection locallyScopedConn = this.connection;

            this.retrieveGeneratedKeys = false;

            checkNullOrEmptyQuery(sql);

            resetCancelledState();

            implicitlyCloseAllOpenResults();

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

                    return this.results;
                }
            }

            setupStreamingTimeout(locallyScopedConn);

            if (this.doEscapeProcessing) {
                Object escapedSqlResult = EscapeProcessor.escapeSQL(sql, locallyScopedConn.serverSupportsConvertFn(), this.connection);

                if (escapedSqlResult instanceof String) {
                    sql = (String) escapedSqlResult;
                } else {
                    sql = ((EscapeProcessorResult) escapedSqlResult).escapedSql;
                }
            }

            char firstStatementChar = StringUtils.firstAlphaCharUc(sql, findStartOfStatement(sql));

            checkForDml(sql, firstStatementChar);

            CachedResultSetMetaData cachedMetaData = null;

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

                return this.results;
            }

            CancelTask timeoutTask = null;

            String oldCatalog = null;

            try {
                if (locallyScopedConn.getEnableQueryTimeouts() && this.timeoutInMillis != 0 && locallyScopedConn.versionMeetsMinimum(5, 0, 0)) {
                    timeoutTask = new CancelTask(this);
                    locallyScopedConn.getCancelTimer().schedule(timeoutTask, this.timeoutInMillis);
                }

                if (!locallyScopedConn.getCatalog().equals(this.currentCatalog)) {
                    oldCatalog = locallyScopedConn.getCatalog();
                    locallyScopedConn.setCatalog(this.currentCatalog);
                }

                //
                // Check if we have cached metadata for this query...
                //

                Field[] cachedFields = null;

                if (locallyScopedConn.getCacheResultSetMetadata()) {
                    cachedMetaData = locallyScopedConn.getCachedMetaData(sql);

                    if (cachedMetaData != null) {
                        cachedFields = cachedMetaData.fields;
                    }
                }

                locallyScopedConn.setSessionMaxRows(this.maxRows);

                statementBegins();

                this.results = locallyScopedConn.execSQL(this, sql, this.maxRows, null, this.resultSetType, this.resultSetConcurrency,
                        createStreamingResultSet(), this.currentCatalog, cachedFields);

                if (timeoutTask != null) {
                    if (timeoutTask.caughtWhileCancelling != null) {
                        throw timeoutTask.caughtWhileCancelling;
                    }

                    timeoutTask.cancel();

                    locallyScopedConn.getCancelTimer().purge();

                    timeoutTask = null;
                }

                synchronized (this.cancelTimeoutMutex) {
                    if (this.wasCancelled) {
                        SQLException cause = null;

                        if (this.wasCancelledByTimeout) {
                            cause = new MySQLTimeoutException();
                        } else {
                            cause = new MySQLStatementCancelledException();
                        }

                        resetCancelledState();

                        throw cause;
                    }
                }
            } finally {
                this.statementExecuting.set(false);

                if (timeoutTask != null) {
                    timeoutTask.cancel();

                    locallyScopedConn.getCancelTimer().purge();
                }

                if (oldCatalog != null) {
                    locallyScopedConn.setCatalog(oldCatalog);
                }
            }

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

            if (cachedMetaData != null) {
                locallyScopedConn.initializeResultsMetadataFromCache(sql, cachedMetaData, this.results);
            } else {
                if (this.connection.getCacheResultSetMetadata()) {
                    locallyScopedConn.initializeResultsMetadataFromCache(sql, null /* will be created */, this.results);
                }
            }

            return this.results;
        }
    }

    protected void doPingInstead() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            if (this.pingTarget != null) {
                this.pingTarget.doPing();
            } else {
                this.connection.ping();
            }

            ResultSetInternalMethods fakeSelectOneResultSet = generatePingResultSet();
            this.results = fakeSelectOneResultSet;
        }
    }

    protected ResultSetInternalMethods generatePingResultSet() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            Field[] fields = { new Field(null, "1", Types.BIGINT, 1) };
            ArrayList<ResultSetRow> rows = new ArrayList<ResultSetRow>();
            byte[] colVal = new byte[] { (byte) '1' };

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

            return (ResultSetInternalMethods) DatabaseMetaData.buildResultSet(fields, rows, this.connection);
        }
    }

    protected void executeSimpleNonQuery(MySQLConnection c, String nonQuery) throws SQLException {
        c.execSQL(this, nonQuery, -1, null, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, false, this.currentCatalog, null, false).close();
    }

    /**
     * Execute a SQL INSERT, UPDATE or DELETE statement. In addition SQL statements that return nothing such as SQL DDL statements can be executed.
     * 
     * @param sql
     *            a SQL statement
     * 
     * @return either a row count, or 0 for SQL commands
     * 
     * @exception SQLException
     *                if a database access error occurs
     */
    public int executeUpdate(String sql) throws SQLException {
        return Util.truncateAndConvertToInt(executeLargeUpdate(sql));
    }

    protected long executeUpdateInternal(String sql, boolean isBatch, boolean returnGeneratedKeys) throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            MySQLConnection locallyScopedConn = this.connection;

            checkNullOrEmptyQuery(sql);

            resetCancelledState();

            char firstStatementChar = StringUtils.firstAlphaCharUc(sql, findStartOfStatement(sql));

            this.retrieveGeneratedKeys = returnGeneratedKeys;

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

            ResultSetInternalMethods rs = null;

            if (this.doEscapeProcessing) {
                Object escapedSqlResult = EscapeProcessor.escapeSQL(sql, this.connection.serverSupportsConvertFn(), this.connection);

                if (escapedSqlResult instanceof String) {
                    sql = (String) escapedSqlResult;
                } else {
                    sql = ((EscapeProcessorResult) escapedSqlResult).escapedSql;
                }
            }

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

            if (StringUtils.startsWithIgnoreCaseAndWs(sql, "select")) {
                throw SQLError.createSQLException(Messages.getString("Statement.46"), "01S03", getExceptionInterceptor());
            }

            implicitlyCloseAllOpenResults();

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

            CancelTask timeoutTask = null;

            String oldCatalog = null;

            boolean readInfoMsgState = locallyScopedConn.isReadInfoMsgEnabled();
            if (returnGeneratedKeys && firstStatementChar == 'R') {
                // If this is a 'REPLACE' query, we need to be able to parse the 'info' message returned from the server to determine the actual number of keys
                // generated.
                locallyScopedConn.setReadInfoMsgEnabled(true);
            }

            try {
                if (locallyScopedConn.getEnableQueryTimeouts() && this.timeoutInMillis != 0 && locallyScopedConn.versionMeetsMinimum(5, 0, 0)) {
                    timeoutTask = new CancelTask(this);
                    locallyScopedConn.getCancelTimer().schedule(timeoutTask, this.timeoutInMillis);
                }

                if (!locallyScopedConn.getCatalog().equals(this.currentCatalog)) {
                    oldCatalog = locallyScopedConn.getCatalog();
                    locallyScopedConn.setCatalog(this.currentCatalog);
                }

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

                statementBegins();

                // null catalog: force read of field info on DML
                rs = locallyScopedConn.execSQL(this, sql, -1, null, java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.CONCUR_READ_ONLY, false,
                        this.currentCatalog, null, isBatch);

                if (timeoutTask != null) {
                    if (timeoutTask.caughtWhileCancelling != null) {
                        throw timeoutTask.caughtWhileCancelling;
                    }

                    timeoutTask.cancel();

                    locallyScopedConn.getCancelTimer().purge();

                    timeoutTask = null;
                }

                synchronized (this.cancelTimeoutMutex) {
                    if (this.wasCancelled) {
                        SQLException cause = null;

                        if (this.wasCancelledByTimeout) {
                            cause = new MySQLTimeoutException();
                        } else {
                            cause = new MySQLStatementCancelledException();
                        }

                        resetCancelledState();

                        throw cause;
                    }
                }
            } finally {
                locallyScopedConn.setReadInfoMsgEnabled(readInfoMsgState);

                if (timeoutTask != null) {
                    timeoutTask.cancel();

                    locallyScopedConn.getCancelTimer().purge();
                }

                if (oldCatalog != null) {
                    locallyScopedConn.setCatalog(oldCatalog);
                }

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

            this.results = rs;

            rs.setFirstCharOfQuery(firstStatementChar);

            this.updateCount = rs.getUpdateCount();

            this.lastInsertId = rs.getUpdateID();

            return this.updateCount;
        }
    }

    /**
     * @see StatementImpl#executeUpdate(String, int)
     */
    public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
        return Util.truncateAndConvertToInt(executeLargeUpdate(sql, autoGeneratedKeys));
    }

    /**
     * @see StatementImpl#executeUpdate(String, int[])
     */
    public int executeUpdate(String sql, int[] columnIndexes) throws SQLException {
        return Util.truncateAndConvertToInt(executeLargeUpdate(sql, columnIndexes));
    }

    /**
     * @see StatementImpl#executeUpdate(String, String[])
     */
    public int executeUpdate(String sql, String[] columnNames) throws SQLException {
        return Util.truncateAndConvertToInt(executeLargeUpdate(sql, columnNames));
    }

    /**
     * Optimization to only use one calendar per-session, or calculate it for
     * each call, depending on user configuration
     */
    protected Calendar getCalendarInstanceForSessionOrNew() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            if (this.connection != null) {
                return this.connection.getCalendarInstanceForSessionOrNew();
            }
            // punt, no connection around
            return new GregorianCalendar();
        }
    }

    /**
     * JDBC 2.0 Return the Connection that produced the Statement.
     * 
     * @return the Connection that produced the Statement
     * 
     * @throws SQLException
     *             if an error occurs
     */
    public java.sql.Connection getConnection() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            return this.connection;
        }
    }

    /**
     * JDBC 2.0 Determine the fetch direction.
     * 
     * @return the default fetch direction
     * 
     * @exception SQLException
     *                if a database-access error occurs
     */
    public int getFetchDirection() throws SQLException {
        return java.sql.ResultSet.FETCH_FORWARD;
    }

    /**
     * JDBC 2.0 Determine the default fetch size.
     * 
     * @return the number of rows to fetch at a time
     * 
     * @throws SQLException
     *             if an error occurs
     */
    public int getFetchSize() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            return this.fetchSize;
        }
    }

    /**
     * @throws SQLException
     */
    public java.sql.ResultSet getGeneratedKeys() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            if (!this.retrieveGeneratedKeys) {
                throw SQLError.createSQLException(Messages.getString("Statement.GeneratedKeysNotRequested"), SQLError.SQL_STATE_ILLEGAL_ARGUMENT,
                        getExceptionInterceptor());
            }

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

            Field[] fields = new Field[1];
            fields[0] = new Field("", "GENERATED_KEY", Types.BIGINT, 20);
            fields[0].setConnection(this.connection);

            this.generatedKeysResults = com.mysql.jdbc.ResultSetImpl.getInstance(this.currentCatalog, fields, new RowDataStatic(this.batchedGeneratedKeys),
                    this.connection, this, false);

            return this.generatedKeysResults;
        }
    }

    /*
     * 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 {
        synchronized (checkClosed().getConnectionMutex()) {
            Field[] fields = new Field[1];
            fields[0] = new Field("", "GENERATED_KEY", Types.BIGINT, 20);
            fields[0].setConnection(this.connection);
            fields[0].setUseOldNameMetadata(true);

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

            long beginAt = getLastInsertID();

            if (beginAt < 0) { // looking at an UNSIGNED BIGINT that has overflowed
                fields[0].setUnsigned();
            }

            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();
                    }
                }
            }

            com.mysql.jdbc.ResultSetImpl gkRs = com.mysql.jdbc.ResultSetImpl.getInstance(this.currentCatalog, fields, new RowDataStatic(rowSet),
                    this.connection, this, false);

            return gkRs;
        }
    }

    public int getId() {
        return this.statementId;
    }

    /**
     * getLastInsertID returns the value of the auto_incremented key after an
     * executeQuery() or excute() 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() {
        try {
            synchronized (checkClosed().getConnectionMutex()) {
                return this.lastInsertId;
            }
        } catch (SQLException e) {
            throw new RuntimeException(e); // evolve interface to throw SQLException
        }
    }

    /**
     * getLongUpdateCount returns the current result as an update count, if the
     * result is a ResultSet or there are no more results, -1 is returned. It
     * should only be called once per result.
     * 
     * <p>
     * This method returns longs as MySQL server versions newer than 3.22.4 return 64-bit values for update counts
     * </p>
     * 
     * @return the current update count.
     */
    public long getLongUpdateCount() {
        try {
            synchronized (checkClosed().getConnectionMutex()) {
                if (this.results == null) {
                    return -1;
                }

                if (this.results.reallyResult()) {
                    return -1;
                }

                return this.updateCount;
            }
        } catch (SQLException e) {
            throw new RuntimeException(e); // evolve interface to throw SQLException
        }
    }

    /**
     * The maxFieldSize limit (in bytes) is the maximum amount of data returned
     * for any column value; it only applies to BINARY, VARBINARY,
     * LONGVARBINARY, CHAR, VARCHAR and LONGVARCHAR columns. If the limit is
     * exceeded, the excess data is silently discarded.
     * 
     * @return the current max column size limit; zero means unlimited
     * 
     * @exception SQLException
     *                if a database access error occurs
     */
    public int getMaxFieldSize() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            return this.maxFieldSize;
        }
    }

    /**
     * The maxRows limit is set to limit the number of rows that any ResultSet
     * can contain. If the limit is exceeded, the excess rows are silently
     * dropped.
     * 
     * @return the current maximum row limit; zero means unlimited
     * 
     * @exception SQLException
     *                if a database access error occurs
     */
    public int getMaxRows() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            if (this.maxRows <= 0) {
                return 0;
            }

            return this.maxRows;
        }
    }

    /**
     * getMoreResults moves to a Statement's next result. If it returns true,
     * this result is a ResulSet.
     * 
     * @return true if the next ResultSet is valid
     * 
     * @exception SQLException
     *                if a database access error occurs
     */
    public boolean getMoreResults() throws SQLException {
        return getMoreResults(CLOSE_CURRENT_RESULT);
    }

    /**
     * @see StatementImpl#getMoreResults(int)
     */
    public boolean getMoreResults(int current) throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            if (this.results == null) {
                return false;
            }

            boolean streamingMode = createStreamingResultSet();

            if (streamingMode) {
                if (this.results.reallyResult()) {
                    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 = this.results.getNextResultSet();

            switch (current) {
                case java.sql.Statement.CLOSE_CURRENT_RESULT:

                    if (this.results != null) {
                        if (!(streamingMode || this.connection.getDontTrackOpenResources())) {
                            this.results.realClose(false);
                        }

                        this.results.clearNextResult();
                    }

                    break;

                case java.sql.Statement.CLOSE_ALL_RESULTS:

                    if (this.results != null) {
                        if (!(streamingMode || this.connection.getDontTrackOpenResources())) {
                            this.results.realClose(false);
                        }

                        this.results.clearNextResult();
                    }

                    closeAllOpenResults();

                    break;

                case java.sql.Statement.KEEP_CURRENT_RESULT:
                    if (!this.connection.getDontTrackOpenResources()) {
                        this.openResults.add(this.results);
                    }

                    this.results.clearNextResult(); // nobody besides us should
                    // ever need this value...
                    break;

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

            this.results = nextResultSet;

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

            boolean moreResults = (this.results != null) && this.results.reallyResult();
            if (!moreResults) {
                checkAndPerformCloseOnCompletionAction();
            }
            return moreResults;
        }
    }

    /**
     * The queryTimeout limit is the number of seconds the driver will wait for
     * a Statement to execute. If the limit is exceeded, a SQLException is
     * thrown.
     * 
     * @return the current query timeout limit in seconds; 0 = unlimited
     * 
     * @exception SQLException
     *                if a database access error occurs
     */
    public int getQueryTimeout() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            return this.timeoutInMillis / 1000;
        }
    }

    /**
     * Parses actual record count from 'info' message
     * 
     * @param serverInfo
     */
    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;
    }

    /**
     * getResultSet returns the current result as a ResultSet. It should only be
     * called once per result.
     * 
     * @return the current result set; null if there are no more
     * 
     * @exception SQLException
     *                if a database access error occurs (why?)
     */
    public java.sql.ResultSet getResultSet() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            return ((this.results != null) && this.results.reallyResult()) ? (java.sql.ResultSet) this.results : null;
        }
    }

    /**
     * JDBC 2.0 Determine the result set concurrency.
     * 
     * @return CONCUR_UPDATABLE or CONCUR_READONLY
     * 
     * @throws SQLException
     *             if an error occurs
     */
    public int getResultSetConcurrency() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            return this.resultSetConcurrency;
        }
    }

    /**
     * @see StatementImpl#getResultSetHoldability()
     */
    public int getResultSetHoldability() throws SQLException {
        return java.sql.ResultSet.HOLD_CURSORS_OVER_COMMIT;
    }

    protected ResultSetInternalMethods getResultSetInternal() {
        try {
            synchronized (checkClosed().getConnectionMutex()) {
                return this.results;
            }
        } catch (SQLException e) {
            return this.results; // you end up with the same thing as before, you'll get exception when actually trying to use it
        }
    }

    /**
     * JDBC 2.0 Determine the result set type.
     * 
     * @return the ResultSet type (SCROLL_SENSITIVE or SCROLL_INSENSITIVE)
     * 
     * @throws SQLException
     *             if an error occurs.
     */
    public int getResultSetType() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            return this.resultSetType;
        }
    }

    /**
     * getUpdateCount returns the current result as an update count, if the
     * result is a ResultSet or there are no more results, -1 is returned. It
     * should only be called once per result.
     * 
     * @return the current result as an update count.
     * 
     * @exception SQLException
     *                if a database access error occurs
     */
    public int getUpdateCount() throws SQLException {
        return Util.truncateAndConvertToInt(getLargeUpdateCount());
    }

    /**
     * The first warning reported by calls on this Statement is returned. A
     * Statement's execute methods clear its java.sql.SQLWarning chain.
     * Subsequent Statement warnings will be chained to this
     * java.sql.SQLWarning.
     * 
     * <p>
     * The Warning chain is automatically cleared each time a statement is (re)executed.
     * </p>
     * 
     * <p>
     * <B>Note:</B> If you are processing a ResultSet then any warnings associated with ResultSet reads will be chained on the ResultSet object.
     * </p>
     * 
     * @return the first java.sql.SQLWarning or null
     * 
     * @exception SQLException
     *                if a database access error occurs
     */
    public java.sql.SQLWarning getWarnings() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {

            if (this.clearWarningsCalled) {
                return null;
            }

            if (this.connection.versionMeetsMinimum(4, 1, 0)) {
                SQLWarning pendingWarningsFromServer = SQLError.convertShowWarningsToSQLWarnings(this.connection);

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

                return this.warningChain;
            }

            return this.warningChain;
        }
    }

    /**
     * Closes this statement, and frees resources.
     * 
     * @param calledExplicitly
     *            was this called from close()?
     * 
     * @throws SQLException
     *             if an error occurs
     */
    protected void realClose(boolean calledExplicitly, boolean closeOpenResults) throws SQLException {
        MySQLConnection 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 (!locallyScopedConn.getDontTrackOpenResources()) {
            locallyScopedConn.unregisterStatement(this);
        }

        if (this.useUsageAdvisor) {
            if (!calledExplicitly) {
                this.connection.getProfilerEventHandlerInstance().processEvent(ProfilerEvent.TYPE_USAGE, this.connection, this, null, 0, new Throwable(),
                        Messages.getString("Statement.63"));
            }
        }

        if (closeOpenResults) {
            closeOpenResults = !(this.holdResultsOpenOverClose || this.connection.getDontTrackOpenResources());
        }

        if (closeOpenResults) {
            if (this.results != null) {

                try {
                    this.results.close();
                } catch (Exception ex) {
                }
            }

            if (this.generatedKeysResults != null) {

                try {
                    this.generatedKeysResults.close();
                } catch (Exception ex) {
                }
            }

            closeAllOpenResults();
        }

        this.isClosed = true;

        this.results = null;
        this.generatedKeysResults = null;
        this.connection = null;
        this.warningChain = null;
        this.openResults = null;
        this.batchedGeneratedKeys = null;
        this.localInfileInputStream = null;
        this.pingTarget = null;
    }

    /**
     * setCursorName defines the SQL cursor name that will be used by subsequent
     * execute methods. This name can then be used in SQL positioned
     * update/delete statements to identify the current row in the ResultSet
     * generated by this statement. If a database doesn't support positioned
     * update/delete, this method is a no-op.
     * 
     * <p>
     * <b>Note:</b> This MySQL driver does not support cursors.
     * </p>
     * 
     * @param name
     *            the new cursor name
     * 
     * @exception SQLException
     *                if a database access error occurs
     */
    public void setCursorName(String name) throws SQLException {
        // No-op
    }

    /**
     * If escape scanning is on (the default), the driver will do escape
     * substitution before sending the SQL to the database.
     * 
     * @param enable
     *            true to enable; false to disable
     * 
     * @exception SQLException
     *                if a database access error occurs
     */
    public void setEscapeProcessing(boolean enable) throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            this.doEscapeProcessing = enable;
        }
    }

    /**
     * JDBC 2.0 Give a hint as to the direction in which the rows in a result
     * set will be processed. The hint applies only to result sets created using
     * this Statement object. The default value is ResultSet.FETCH_FORWARD.
     * 
     * @param direction
     *            the initial direction for processing rows
     * 
     * @exception SQLException
     *                if a database-access error occurs or direction is not one
     *                of ResultSet.FETCH_FORWARD, ResultSet.FETCH_REVERSE, or
     *                ResultSet.FETCH_UNKNOWN
     */
    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"), SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
        }
    }

    /**
     * JDBC 2.0 Give the JDBC driver a hint as to the number of rows that should
     * be fetched from the database when more rows are needed. The number of
     * rows specified only affects result sets created using this statement. If
     * the value specified is zero, then the hint is ignored. The default value
     * is zero.
     * 
     * @param rows
     *            the number of rows to fetch
     * 
     * @exception SQLException
     *                if a database-access error occurs, or the condition 0
     *                &lt;= rows &lt;= this.getMaxRows() is not satisfied.
     */
    public void setFetchSize(int rows) throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            if (((rows < 0) && (rows != Integer.MIN_VALUE)) || ((this.maxRows > 0) && (rows > this.getMaxRows()))) {
                throw SQLError.createSQLException(Messages.getString("Statement.7"), SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
            }

            this.fetchSize = rows;
        }
    }

    public void setHoldResultsOpenOverClose(boolean holdResultsOpenOverClose) {
        try {
            synchronized (checkClosed().getConnectionMutex()) {
                this.holdResultsOpenOverClose = holdResultsOpenOverClose;
            }
        } catch (SQLException e) {
            // FIXME: can't break interface at this point
        }
    }

    /**
     * Sets the maxFieldSize
     * 
     * @param max
     *            the new max column size limit; zero means unlimited
     * 
     * @exception SQLException
     *                if size exceeds buffer size
     */
    public void setMaxFieldSize(int max) throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            if (max < 0) {
                throw SQLError.createSQLException(Messages.getString("Statement.11"), SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
            }

            int maxBuf = (this.connection != null) ? this.connection.getMaxAllowedPacket() : MysqlIO.getMaxBuf();

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

            this.maxFieldSize = max;
        }
    }

    /**
     * Set the maximum number of rows
     * 
     * @param max
     *            the new max rows limit; zero means unlimited
     * 
     * @exception SQLException
     *                if a database access error occurs
     * 
     * @see getMaxRows
     */
    public void setMaxRows(int max) throws SQLException {
        setLargeMaxRows(max);
    }

    /**
     * Sets the queryTimeout limit
     * 
     * @param seconds
     *            -
     *            the new query timeout limit in seconds
     * 
     * @exception SQLException
     *                if a database access error occurs
     */
    public void setQueryTimeout(int seconds) throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            if (seconds < 0) {
                throw SQLError.createSQLException(Messages.getString("Statement.21"), SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
            }

            this.timeoutInMillis = seconds * 1000;
        }
    }

    /**
     * Sets the concurrency for result sets generated by this statement
     * 
     * @param concurrencyFlag
     */
    void setResultSetConcurrency(int concurrencyFlag) {
        try {
            synchronized (checkClosed().getConnectionMutex()) {
                this.resultSetConcurrency = concurrencyFlag;
            }
        } catch (SQLException 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
     */
    void setResultSetType(int typeFlag) {
        try {
            synchronized (checkClosed().getConnectionMutex()) {
                this.resultSetType = typeFlag;
            }
        } catch (SQLException e) {
            // FIXME: Can't break interface atm, we'll get the exception later when you try and do something useful with a closed statement...
        }
    }

    protected void getBatchedGeneratedKeys(java.sql.Statement batchedStatement) throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            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();
                    }
                }
            }
        }
    }

    protected void getBatchedGeneratedKeys(int maxKeys) throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            if (this.retrieveGeneratedKeys) {
                java.sql.ResultSet rs = null;

                try {
                    if (maxKeys == 0) {
                        rs = getGeneratedKeysInternal();
                    } else {
                        rs = getGeneratedKeysInternal(maxKeys);
                    }

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

    private boolean useServerFetch() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            return this.connection.isCursorFetchEnabled() && this.fetchSize > 0 && this.resultSetConcurrency == ResultSet.CONCUR_READ_ONLY
                    && this.resultSetType == ResultSet.TYPE_FORWARD_ONLY;
        }
    }

    public boolean isClosed() throws SQLException {
        MySQLConnection locallyScopedConn = this.connection;
        if (locallyScopedConn == null) {
            return true;
        }
        synchronized (locallyScopedConn.getConnectionMutex()) {
            return this.isClosed;
        }
    }

    private boolean isPoolable = true;

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

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

    /**
     * @see java.sql.Wrapper#isWrapperFor(Class)
     */
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        checkClosed();

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

    /**
     * @see java.sql.Wrapper#unwrap(Class)
     */
    public <T> T unwrap(Class<T> iface) throws java.sql.SQLException {
        try {
            // This works for classes that aren't actually wrapping anything
            return iface.cast(this);
        } catch (ClassCastException cce) {
            throw SQLError.createSQLException("Unable to unwrap to " + iface.toString(), SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
        }
    }

    protected static int findStartOfStatement(String sql) {
        int statementStartPos = 0;

        if (StringUtils.startsWithIgnoreCaseAndWs(sql, "/*")) {
            statementStartPos = sql.indexOf("*/");

            if (statementStartPos == -1) {
                statementStartPos = 0;
            } else {
                statementStartPos += 2;
            }
        } else if (StringUtils.startsWithIgnoreCaseAndWs(sql, "--") || StringUtils.startsWithIgnoreCaseAndWs(sql, "#")) {
            statementStartPos = sql.indexOf('\n');

            if (statementStartPos == -1) {
                statementStartPos = sql.indexOf('\r');

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

        return statementStartPos;
    }

    private InputStream localInfileInputStream;

    protected final boolean version5013OrNewer;

    public InputStream getLocalInfileInputStream() {
        return this.localInfileInputStream;
    }

    public void setLocalInfileInputStream(InputStream stream) {
        this.localInfileInputStream = stream;
    }

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

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

    protected boolean containsOnDuplicateKeyInString(String sql) {
        return getOnDuplicateKeyLocation(sql, this.connection.getDontCheckOnDuplicateKeyUpdateInSQL(), this.connection.getRewriteBatchedStatements(),
                this.connection.isNoBackslashEscapesSet()) != -1;
    }

    protected static int getOnDuplicateKeyLocation(String sql, boolean dontCheckOnDuplicateKeyUpdateInSQL, boolean rewriteBatchedStatements,
            boolean noBackslashEscapes) {
        return dontCheckOnDuplicateKeyUpdateInSQL && !rewriteBatchedStatements ? -1
                : StringUtils.indexOfIgnoreCase(0, sql, ON_DUPLICATE_KEY_UPDATE_CLAUSE, "\"'`", "\"'`",
                        noBackslashEscapes ? StringUtils.SEARCH_MODE__MRK_COM_WS : StringUtils.SEARCH_MODE__ALL);
    }

    private boolean closeOnCompletion = false;

    public void closeOnCompletion() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            this.closeOnCompletion = true;
        }
    }

    public boolean isCloseOnCompletion() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            return this.closeOnCompletion;
        }
    }

    /**
     * JDBC 4.2
     * Same as {@link #executeBatch()} but returns long[] instead of int[].
     */
    public long[] executeLargeBatch() throws SQLException {
        return executeBatchInternal();
    }

    /**
     * JDBC 4.2
     * Same as {@link #executeUpdate(String)} but returns long instead of int.
     */
    public long executeLargeUpdate(String sql) throws SQLException {
        return executeUpdateInternal(sql, false, false);
    }

    /**
     * JDBC 4.2
     * Same as {@link #executeUpdate(String, int)} but returns long instead of int.
     */
    public long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
        return executeUpdateInternal(sql, false, autoGeneratedKeys == java.sql.Statement.RETURN_GENERATED_KEYS);
    }

    /**
     * JDBC 4.2
     * Same as {@link #executeUpdate(String, int[])} but returns long instead of int.
     */
    public long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLException {
        return executeUpdateInternal(sql, false, columnIndexes != null && columnIndexes.length > 0);
    }

    /**
     * JDBC 4.2
     * Same as {@link #executeUpdate(String, String[])} but returns long instead of int.
     */
    public long executeLargeUpdate(String sql, String[] columnNames) throws SQLException {
        return executeUpdateInternal(sql, false, columnNames != null && columnNames.length > 0);
    }

    /**
     * JDBC 4.2
     * Same as {@link #getMaxRows()} but returns long instead of int.
     */
    public long getLargeMaxRows() throws SQLException {
        // Max rows is limited by MySQLDefs.MAX_ROWS anyway...
        return getMaxRows();
    }

    /**
     * JDBC 4.2
     * Same as {@link #getUpdateCount()} but returns long instead of int;
     */
    public long getLargeUpdateCount() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            if (this.results == null) {
                return -1;
            }

            if (this.results.reallyResult()) {
                return -1;
            }

            return this.results.getUpdateCount();
        }
    }

    /**
     * JDBC 4.2
     * Same as {@link #setMaxRows(int)} but accepts a long value instead of an int.
     */
    public void setLargeMaxRows(long max) throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            if ((max > MysqlDefs.MAX_ROWS) || (max < 0)) {
                throw SQLError.createSQLException(Messages.getString("Statement.15") + max + " > " + MysqlDefs.MAX_ROWS + ".",
                        SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
            }

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

            this.maxRows = (int) max;
        }
    }

    boolean isCursorRequired() throws SQLException {
        return false;
    }
}
