content: /*
   * Copyright (c) 2002, 2022, Oracle and/or its affiliates.
   *
   * This program is free software; you can redistribute it and/or modify it under
   * the terms of the GNU General Public License, version 2.0, as published by the
   * Free Software Foundation.
   *
   * This program is also distributed with certain software (including but not
   * limited to OpenSSL) that is licensed under separate terms, as designated in a
   * particular file or component or in included license documentation. The
   * authors of MySQL hereby grant you an additional permission to link the
   * program and your derivative works with the separately licensed software that
   * they have included with MySQL.
   *
   * Without limiting anything contained in the foregoing, this file, which is
   * part of MySQL Connector/J, is also subject to the Universal FOSS Exception,
   * version 1.0, a copy of which can be found at
   * http://oss.oracle.com/licenses/universal-foss-exception.
   *
   * This program is distributed in the hope that it will be useful, but WITHOUT
   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
   * FOR A PARTICULAR PURPOSE. See the GNU General Public License, version 2.0,
   * for more details.
   *
   * You should have received a copy of the GNU General Public License along with
   * this program; if not, write to the Free Software Foundation, Inc.,
   * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
   */
  
  package com.mysql.cj.jdbc;
  
  import java.io.IOException;
  import java.io.InputStream;
  import java.math.BigInteger;
  import java.sql.BatchUpdateException;
  import java.sql.ResultSet;
  import java.sql.SQLException;
  import java.sql.SQLWarning;
  import java.util.ArrayList;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Set;
  import java.util.concurrent.atomic.AtomicBoolean;
  
  import com.mysql.cj.CancelQueryTask;
  import com.mysql.cj.Messages;
  import com.mysql.cj.MysqlType;
  import com.mysql.cj.NativeSession;
  import com.mysql.cj.PingTarget;
  import com.mysql.cj.Query;
  import com.mysql.cj.QueryAttributesBindings;
  import com.mysql.cj.QueryInfo;
  import com.mysql.cj.QueryReturnType;
  import com.mysql.cj.Session;
  import com.mysql.cj.SimpleQuery;
  import com.mysql.cj.TransactionEventHandler;
  import com.mysql.cj.conf.HostInfo;
  import com.mysql.cj.conf.PropertyDefinitions;
  import com.mysql.cj.conf.PropertyKey;
  import com.mysql.cj.conf.RuntimeProperty;
  import com.mysql.cj.exceptions.AssertionFailedException;
  import com.mysql.cj.exceptions.CJException;
  import com.mysql.cj.exceptions.CJOperationNotSupportedException;
  import com.mysql.cj.exceptions.CJTimeoutException;
  import com.mysql.cj.exceptions.ExceptionFactory;
  import com.mysql.cj.exceptions.ExceptionInterceptor;
  import com.mysql.cj.exceptions.MysqlErrorNumbers;
  import com.mysql.cj.exceptions.OperationCancelledException;
  import com.mysql.cj.exceptions.StatementIsClosedException;
  import com.mysql.cj.jdbc.exceptions.MySQLStatementCancelledException;
  import com.mysql.cj.jdbc.exceptions.MySQLTimeoutException;
  import com.mysql.cj.jdbc.exceptions.SQLError;
  import com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping;
  import com.mysql.cj.jdbc.result.CachedResultSetMetaData;
  import com.mysql.cj.jdbc.result.ResultSetFactory;
  import com.mysql.cj.jdbc.result.ResultSetImpl;
  import com.mysql.cj.jdbc.result.ResultSetInternalMethods;
  import com.mysql.cj.log.ProfilerEvent;
  import com.mysql.cj.protocol.Message;
  import com.mysql.cj.protocol.ProtocolEntityFactory;
  import com.mysql.cj.protocol.Resultset;
  import com.mysql.cj.protocol.Resultset.Type;
  import com.mysql.cj.protocol.a.NativeConstants;
  import com.mysql.cj.protocol.a.NativeMessageBuilder;
  import com.mysql.cj.protocol.a.result.ByteArrayRow;
  import com.mysql.cj.protocol.a.result.ResultsetRowsStatic;
  import com.mysql.cj.result.DefaultColumnDefinition;
  import com.mysql.cj.result.Field;
  import com.mysql.cj.result.Row;
  import com.mysql.cj.util.StringUtils;
  import com.mysql.cj.util.Util;
  
  /**
   * A Statement object is used for executing a static SQL statement and obtaining
   * the results produced by it.
   * 
   * Only one ResultSet per Statement can be open at any point in time. Therefore, if the reading of one ResultSet is interleaved with the reading of another,
   * each must have been generated by different Statements. All statement execute methods implicitly close a statement's current ResultSet if an open one exists.
   */
  public class StatementImpl implements JdbcStatement {
      protected static final String PING_MARKER = "/* ping */";
  
      public final static byte USES_VARIABLES_FALSE = 0;
  
      public final static byte USES_VARIABLES_TRUE = 1;
  
      public final static byte USES_VARIABLES_UNKNOWN = -1;
  
      protected NativeMessageBuilder commandBuilder = null; // TODO use shared builder
  
      /** The character encoding to use (if available) */
      protected String charEncoding = null;
  
      /** The connection that created us */
      protected volatile JdbcConnection connection = null;
  
      /** Should we process escape codes? */
      protected boolean doEscapeProcessing = true;
  
      /** Has this statement been closed? */
      protected boolean isClosed = false;
  
      /** The auto_increment value for the last insert */
      protected long lastInsertId = -1;
  
      /** The max field size for this statement */
      protected int maxFieldSize = (Integer) PropertyDefinitions.getPropertyDefinition(PropertyKey.maxAllowedPacket).getDefaultValue();
  
      /**
       * The maximum number of rows to return for this statement (-1 means _all_
       * rows)
       */
      public int maxRows = -1;
  
      /** Set of currently-open ResultSets */
      protected Set<ResultSetInternalMethods> openResults = new HashSet<>();
  
      /** Are we in pedantic mode? */
      protected boolean pedantic = false;
  
      /** Should we profile? */
      protected boolean profileSQL = false;
  
      /** The current results */
      protected ResultSetInternalMethods results = null;
  
      protected ResultSetInternalMethods generatedKeysResults = null;
  
      /** The concurrency for this result set (updatable or not) */
      protected int resultSetConcurrency = 0;
  
      /** The update count for this statement */
      protected long updateCount = -1;
  
      /** Should we use the usage advisor? */
      protected boolean useUsageAdvisor = false;
  
      /** The warnings chain. */
      protected SQLWarning warningChain = null;
  
      /**
       * Should this statement hold results open over .close() irregardless of
       * connection's setting?
       */
      protected boolean holdResultsOpenOverClose = false;
  
      protected ArrayList<Row> batchedGeneratedKeys = null;
  
      protected boolean retrieveGeneratedKeys = false;
  
      protected boolean continueBatchOnError = false;
  
      protected PingTarget pingTarget = null;
  
      protected ExceptionInterceptor exceptionInterceptor;
  
      /** Whether or not the last query was of the form ON DUPLICATE KEY UPDATE */
      protected boolean lastQueryIsOnDupKeyUpdate = false;
  
      /** Are we currently closing results implicitly (internally)? */
      private boolean isImplicitlyClosingResults = false;
  
      protected RuntimeProperty<Boolean> dontTrackOpenResources;
      protected RuntimeProperty<Boolean> dumpQueriesOnException;
      protected boolean logSlowQueries = false;
      protected RuntimeProperty<Boolean> rewriteBatchedStatements;
      protected RuntimeProperty<Integer> maxAllowedPacket;
      protected boolean dontCheckOnDuplicateKeyUpdateInSQL;
  
      protected ResultSetFactory resultSetFactory;
  
      protected Query query;
      protected NativeSession session = null;
  
      /**
       * Constructor for a Statement.
       * 
       * @param c
       *            the Connection instance that creates us
       * @param db
       *            the database name in use when we were created
       * 
       * @throws SQLException
       *             if an error occurs.
       */
      public StatementImpl(JdbcConnection c, String db) throws SQLException {
          if ((c == null) || c.isClosed()) {
              throw SQLError.createSQLException(Messages.getString("Statement.0"), MysqlErrorNumbers.SQL_STATE_CONNECTION_NOT_OPEN, null);
          }
  
          this.connection = c;
          this.session = (NativeSession) c.getSession();
          this.exceptionInterceptor = c.getExceptionInterceptor();
  
          this.commandBuilder = new NativeMessageBuilder(this.session.getServerSession().supportsQueryAttributes());
  
          try {
              initQuery();
          } catch (CJException e) {
              throw SQLExceptionsMapping.translateException(e, getExceptionInterceptor());
          }
  
          this.query.setCurrentDatabase(db);
  
          JdbcPropertySet pset = c.getPropertySet();
  
          this.dontTrackOpenResources = pset.getBooleanProperty(PropertyKey.dontTrackOpenResources);
          this.dumpQueriesOnException = pset.getBooleanProperty(PropertyKey.dumpQueriesOnException);
          this.continueBatchOnError = pset.getBooleanProperty(PropertyKey.continueBatchOnError).getValue();
          this.pedantic = pset.getBooleanProperty(PropertyKey.pedantic).getValue();
          this.rewriteBatchedStatements = pset.getBooleanProperty(PropertyKey.rewriteBatchedStatements);
          this.charEncoding = pset.getStringProperty(PropertyKey.characterEncoding).getValue();
          this.profileSQL = pset.getBooleanProperty(PropertyKey.profileSQL).getValue();
          this.useUsageAdvisor = pset.getBooleanProperty(PropertyKey.useUsageAdvisor).getValue();
          this.logSlowQueries = pset.getBooleanProperty(PropertyKey.logSlowQueries).getValue();
          this.maxAllowedPacket = pset.getIntegerProperty(PropertyKey.maxAllowedPacket);
          this.dontCheckOnDuplicateKeyUpdateInSQL = pset.getBooleanProperty(PropertyKey.dontCheckOnDuplicateKeyUpdateInSQL).getValue();
          this.doEscapeProcessing = pset.getBooleanProperty(PropertyKey.enableEscapeProcessing).getValue();
  
          this.maxFieldSize = this.maxAllowedPacket.getValue();
  
          if (!this.dontTrackOpenResources.getValue()) {
              c.registerStatement(this);
          }
  
          int defaultFetchSize = pset.getIntegerProperty(PropertyKey.defaultFetchSize).getValue();
          if (defaultFetchSize != 0) {
              setFetchSize(defaultFetchSize);
          }
  
          int maxRowsConn = pset.getIntegerProperty(PropertyKey.maxRows).getValue();
  
          if (maxRowsConn != -1) {
              setMaxRows(maxRowsConn);
          }
  
          this.holdResultsOpenOverClose = pset.getBooleanProperty(PropertyKey.holdResultsOpenOverStatementClose).getValue();
  
          this.resultSetFactory = new ResultSetFactory(this.connection, this);
      }
  
      protected void initQuery() {
          this.query = new SimpleQuery(this.session);
      }
  
      @Override
      public void addBatch(String sql) throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              if (sql != null) {
                  this.query.addBatch(sql);
              }
          }
      }
  
      @Override
      public void addBatch(Object batch) {
          this.query.addBatch(batch);
      }
  
      @Override
      public List<Object> getBatchedArgs() {
          return this.query.getBatchedArgs();
      }
  
      @Override
      public void cancel() throws SQLException {
          if (!this.query.getStatementExecuting().get()) {
              return;
          }
  
          if (!this.isClosed && this.connection != null) {
              NativeSession newSession = null;
  
              try {
                  HostInfo hostInfo = this.session.getHostInfo();
                  String database = hostInfo.getDatabase();
                  String user = hostInfo.getUser();
                  String password = hostInfo.getPassword();
                  newSession = new NativeSession(this.session.getHostInfo(), this.session.getPropertySet());
                  newSession.connect(hostInfo, user, password, database, 30000, new TransactionEventHandler() {
                      @Override
                      public void transactionCompleted() {
                      }
  
                      @Override
                      public void transactionBegun() {
                      }
                  });
                  newSession.getProtocol().sendCommand(new NativeMessageBuilder(newSession.getServerSession().supportsQueryAttributes())
                          .buildComQuery(newSession.getSharedSendPacket(), "KILL QUERY " + this.session.getThreadId()), false, 0);
                  setCancelStatus(CancelStatus.CANCELED_BY_USER);
              } catch (IOException e) {
                  throw SQLExceptionsMapping.translateException(e, this.exceptionInterceptor);
              } finally {
                  if (newSession != null) {
                      newSession.forceClose();
                  }
              }
  
          }
      }
  
      // --------------------------JDBC 2.0-----------------------------
  
      /**
       * Checks if closed() has been called, and throws an exception if so
       * 
       * @return connection
       * @throws StatementIsClosedException
       *             if this statement has been closed
       */
      protected JdbcConnection checkClosed() {
          JdbcConnection c = this.connection;
  
          if (c == null) {
              throw ExceptionFactory.createException(StatementIsClosedException.class, Messages.getString("Statement.AlreadyClosed"), getExceptionInterceptor());
          }
  
          return c;
      }
  
      /**
       * Checks if the given SQL query is a result set producing query.
       * 
       * @param sql
       *            the SQL to check
       * @return
       *         <code>true</code> if the query produces a result set, <code>false</code> otherwise.
       */
      protected boolean isResultSetProducingQuery(String sql) {
          QueryReturnType queryReturnType = QueryInfo.getQueryReturnType(sql, this.session.getServerSession().isNoBackslashEscapesSet());
          return queryReturnType == QueryReturnType.PRODUCES_RESULT_SET || queryReturnType == QueryReturnType.MAY_PRODUCE_RESULT_SET;
      }
  
      /**
       * Checks if the given SQL query does not return a result set.
       * 
       * @param sql
       *            the SQL to check
       * @return
       *         <code>true</code> if the query does not produce a result set, <code>false</code> otherwise.
       */
      protected boolean isNonResultSetProducingQuery(String sql) {
          QueryReturnType queryReturnType = QueryInfo.getQueryReturnType(sql, this.session.getServerSession().isNoBackslashEscapesSet());
          return queryReturnType == QueryReturnType.DOES_NOT_PRODUCE_RESULT_SET || queryReturnType == QueryReturnType.MAY_PRODUCE_RESULT_SET;
      }
  
      /**
       * Method checkNullOrEmptyQuery.
       * 
       * @param sql
       *            the SQL to check
       * 
       * @throws SQLException
       *             if query is null or empty.
       */
      protected void checkNullOrEmptyQuery(String sql) throws SQLException {
          if (sql == null) {
              throw SQLError.createSQLException(Messages.getString("Statement.59"), MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
          }
  
          if (sql.length() == 0) {
              throw SQLError.createSQLException(Messages.getString("Statement.61"), MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
          }
      }
  
      @Override
      public void clearBatch() throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              this.query.clearBatchedArgs();
          }
      }
  
      @Override
      public void clearBatchedArgs() {
          this.query.clearBatchedArgs();
      }
  
      @Override
      public void clearWarnings() throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              setClearWarningsCalled(true);
              this.warningChain = null;
              // TODO souldn't we also clear warnings from _server_ ?
          }
      }
  
      /**
       * 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
       */
      @Override
      public void close() throws SQLException {
          realClose(true, true);
      }
  
      /**
       * Close any open result sets that have been 'held open'
       * 
       * @throws SQLException
       *             if an error occurs
       */
      protected void closeAllOpenResults() throws SQLException {
          JdbcConnection locallyScopedConn = this.connection;
  
          if (locallyScopedConn == null) {
              return; // already closed
          }
  
          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
       * 
       * @throws SQLException
       *             if a database access error occurs
       */
      protected void implicitlyCloseAllOpenResults() throws SQLException {
          this.isImplicitlyClosingResults = true;
          try {
              if (!(this.holdResultsOpenOverClose || this.dontTrackOpenResources.getValue())) {
                  if (this.results != null) {
                      this.results.realClose(false);
                  }
                  if (this.generatedKeysResults != null) {
                      this.generatedKeysResults.realClose(false);
                  }
                  closeAllOpenResults();
              }
          } finally {
              this.isImplicitlyClosingResults = false;
          }
      }
  
      @Override
      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 (StatementIsClosedException e) {
              // we can't break the interface, having this be no-op in case of error is ok
          }
      }
  
      @Override
      public int getOpenResultSetCount() {
          try {
              synchronized (checkClosed().getConnectionMutex()) {
                  if (this.openResults != null) {
                      return this.openResults.size();
                  }
  
                  return 0;
              }
          } catch (StatementIsClosedException e) {
              // we can't break the interface, having this be no-op in case of error is ok
  
              return 0;
          }
      }
  
      /**
       * Check if all ResultSets generated by this statement are closed. If so,
       * close this statement.
       */
      private void checkAndPerformCloseOnCompletionAction() {
          try {
              synchronized (checkClosed().getConnectionMutex()) {
                  if (isCloseOnCompletion() && !this.dontTrackOpenResources.getValue() && getOpenResultSetCount() == 0
                          && (this.results == null || !this.results.hasRows() || this.results.isClosed())
                          && (this.generatedKeysResults == null || !this.generatedKeysResults.hasRows() || this.generatedKeysResults.isClosed())) {
                      realClose(false, false);
                  }
              }
          } catch (SQLException e) {
          }
      }
  
      /**
       * @param sql
       *            query
       * @return result set
       * @throws SQLException
       *             if a database access error occurs or this method is called on a closed Statement
       */
      private ResultSetInternalMethods createResultSetUsingServerFetch(String sql) throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              java.sql.PreparedStatement pStmt = this.connection.prepareStatement(sql, this.query.getResultType().getIntValue(), this.resultSetConcurrency);
  
              pStmt.setFetchSize(this.query.getResultFetchSize());
  
              if (this.getQueryTimeout() > 0) {
                  pStmt.setQueryTimeout(this.getQueryTimeout());
              }
  
              if (this.maxRows > -1) {
                  pStmt.setMaxRows(this.maxRows);
              }
  
              statementBegins();
  
              pStmt.execute();
  
              //
              // Need to be able to get resultset irrespective if we issued DML or not to make this work.
              //
              ResultSetInternalMethods rs = ((JdbcStatement) pStmt).getResultSetInternal();
  
              rs.setStatementUsedForFetchingRows((JdbcPreparedStatement) pStmt);
  
              this.results = rs;
  
              return rs;
          }
      }
  
      /**
       * 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.query.getResultType() == Type.FORWARD_ONLY) && (this.resultSetConcurrency == java.sql.ResultSet.CONCUR_READ_ONLY)
                  && (this.query.getResultFetchSize() == Integer.MIN_VALUE));
      }
  
      private Resultset.Type originalResultSetType = Type.FORWARD_ONLY;
      private int originalFetchSize = 0;
  
      @Override
      public void enableStreamingResults() throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              this.originalResultSetType = this.query.getResultType();
              this.originalFetchSize = this.query.getResultFetchSize();
  
              setFetchSize(Integer.MIN_VALUE);
              setResultSetType(Type.FORWARD_ONLY);
          }
      }
  
      @Override
      public void disableStreamingResults() throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              if (this.query.getResultFetchSize() == Integer.MIN_VALUE && this.query.getResultType() == 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().
       * 
       * @param con
       *            created this statement
       * @throws SQLException
       *             if a database error occurs
       */
      protected void setupStreamingTimeout(JdbcConnection con) throws SQLException {
          int netTimeoutForStreamingResults = this.session.getPropertySet().getIntegerProperty(PropertyKey.netTimeoutForStreamingResults).getValue();
  
          if (createStreamingResultSet() && netTimeoutForStreamingResults > 0) {
              executeSimpleNonQuery(con, "SET net_write_timeout=" + netTimeoutForStreamingResults);
          }
      }
  
      @Override
      public CancelQueryTask startQueryTimer(Query stmtToCancel, int timeout) {
          return this.query.startQueryTimer(stmtToCancel, timeout);
      }
  
      @Override
      public void stopQueryTimer(CancelQueryTask timeoutTask, boolean rethrowCancelReason, boolean checkCancelTimeout) {
          this.query.stopQueryTimer(timeoutTask, rethrowCancelReason, checkCancelTimeout);
      }
  
      @Override
      public boolean execute(String sql) throws SQLException {
          return executeInternal(sql, false);
      }
  
      private boolean executeInternal(String sql, boolean returnGeneratedKeys) throws SQLException {
          JdbcConnection locallyScopedConn = checkClosed();
  
          synchronized (locallyScopedConn.getConnectionMutex()) {
              checkClosed();
  
              checkNullOrEmptyQuery(sql);
  
              resetCancelledState();
  
              implicitlyCloseAllOpenResults();
  
              if (sql.charAt(0) == '/') {
                  if (sql.startsWith(PING_MARKER)) {
                      doPingInstead();
  
                      return true;
                  }
              }
  
              this.retrieveGeneratedKeys = returnGeneratedKeys;
  
              this.lastQueryIsOnDupKeyUpdate = returnGeneratedKeys
                      && QueryInfo.firstCharOfStatementUc(sql, this.session.getServerSession().isNoBackslashEscapesSet()) == 'I'
                      && containsOnDuplicateKeyInString(sql);
  
              if (!QueryInfo.isReadOnlySafeQuery(sql, this.session.getServerSession().isNoBackslashEscapesSet()) && locallyScopedConn.isReadOnly()) {
                  throw SQLError.createSQLException(Messages.getString("Statement.27") + Messages.getString("Statement.28"),
                          MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
              }
  
              try {
                  setupStreamingTimeout(locallyScopedConn);
  
                  if (this.doEscapeProcessing) {
                      Object escapedSqlResult = EscapeProcessor.escapeSQL(sql, this.session.getServerSession().getSessionTimeZone(),
                              this.session.getServerSession().getCapabilities().serverSupportsFracSecs(),
                              this.session.getServerSession().isServerTruncatesFracSecs(), getExceptionInterceptor());
                      sql = escapedSqlResult instanceof String ? (String) escapedSqlResult : ((EscapeProcessorResult) escapedSqlResult).escapedSql;
                  }
  
                  CachedResultSetMetaData cachedMetaData = null;
  
                  ResultSetInternalMethods rs = null;
  
                  this.batchedGeneratedKeys = null;
  
                  if (useServerFetch()) {
                      rs = createResultSetUsingServerFetch(sql);
                  } else {
                      CancelQueryTask timeoutTask = null;
  
                      String oldDb = null;
  
                      try {
                          timeoutTask = startQueryTimer(this, getTimeoutInMillis());
  
                          if (!locallyScopedConn.getDatabase().equals(getCurrentDatabase())) {
                              oldDb = locallyScopedConn.getDatabase();
                              locallyScopedConn.setDatabase(getCurrentDatabase());
                          }
  
                          // Check if we have cached metadata for this query...
                          if (locallyScopedConn.getPropertySet().getBooleanProperty(PropertyKey.cacheResultSetMetadata).getValue()) {
                              cachedMetaData = locallyScopedConn.getCachedMetaData(sql);
                          }
  
                          // Only apply max_rows to selects
                          locallyScopedConn.setSessionMaxRows(isResultSetProducingQuery(sql) ? this.maxRows : -1);
  
                          statementBegins();
  
                          rs = ((NativeSession) locallyScopedConn.getSession()).execSQL(this, sql, this.maxRows, null, createStreamingResultSet(),
                                  getResultSetFactory(), cachedMetaData, false);
  
                          if (timeoutTask != null) {
                              stopQueryTimer(timeoutTask, true, true);
                              timeoutTask = null;
                          }
  
                      } catch (CJTimeoutException | OperationCancelledException e) {
                          throw SQLExceptionsMapping.translateException(e, this.exceptionInterceptor);
  
                      } finally {
                          stopQueryTimer(timeoutTask, false, false);
  
                          if (oldDb != null) {
                              locallyScopedConn.setDatabase(oldDb);
                          }
                      }
                  }
  
                  if (rs != null) {
                      this.lastInsertId = rs.getUpdateID();
  
                      this.results = rs;
  
                      rs.setFirstCharOfQuery(QueryInfo.firstCharOfStatementUc(sql, this.session.getServerSession().isNoBackslashEscapesSet()));
  
                      if (rs.hasRows()) {
                          if (cachedMetaData != null) {
                              locallyScopedConn.initializeResultsMetadataFromCache(sql, cachedMetaData, this.results);
                          } else if (this.session.getPropertySet().getBooleanProperty(PropertyKey.cacheResultSetMetadata).getValue()) {
                              locallyScopedConn.initializeResultsMetadataFromCache(sql, null /* will be created */, this.results);
                          }
                      }
                  }
  
                  return ((rs != null) && rs.hasRows());
              } finally {
                  this.query.getStatementExecuting().set(false);
              }
          }
      }
  
      @Override
      public void statementBegins() {
          this.query.statementBegins();
      }
  
      @Override
      public void resetCancelledState() {
          synchronized (checkClosed().getConnectionMutex()) {
              this.query.resetCancelledState();
          }
      }
  
      @Override
      public boolean execute(String sql, int returnGeneratedKeys) throws SQLException {
          return executeInternal(sql, returnGeneratedKeys == java.sql.Statement.RETURN_GENERATED_KEYS);
      }
  
      @Override
      public boolean execute(String sql, int[] generatedKeyIndices) throws SQLException {
          return executeInternal(sql, generatedKeyIndices != null && generatedKeyIndices.length > 0);
      }
  
      @Override
      public boolean execute(String sql, String[] generatedKeyNames) throws SQLException {
          return executeInternal(sql, generatedKeyNames != null && generatedKeyNames.length > 0);
      }
  
      @Override
      public int[] executeBatch() throws SQLException {
          return Util.truncateAndConvertToInt(executeBatchInternal());
      }
  
      protected long[] executeBatchInternal() throws SQLException {
          JdbcConnection locallyScopedConn = checkClosed();
  
          synchronized (locallyScopedConn.getConnectionMutex()) {
              if (locallyScopedConn.isReadOnly()) {
                  throw SQLError.createSQLException(Messages.getString("Statement.34") + Messages.getString("Statement.35"),
                          MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
              }
  
              implicitlyCloseAllOpenResults();
  
              List<Object> batchedArgs = this.query.getBatchedArgs();
  
              if (batchedArgs == null || batchedArgs.size() == 0) {
                  return new long[0];
              }
  
              // we timeout the entire batch, not individual statements
              int individualStatementTimeout = getTimeoutInMillis();
              setTimeoutInMillis(0);
  
              CancelQueryTask 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 (batchedArgs != null) {
                          int nbrCommands = batchedArgs.size();
  
                          this.batchedGeneratedKeys = new ArrayList<>(batchedArgs.size());
  
                          boolean multiQueriesEnabled = locallyScopedConn.getPropertySet().getBooleanProperty(PropertyKey.allowMultiQueries).getValue();
  
                          if (multiQueriesEnabled || (locallyScopedConn.getPropertySet().getBooleanProperty(PropertyKey.rewriteBatchedStatements).getValue()
                                  && nbrCommands > 4)) {
                              return executeBatchUsingMultiQueries(multiQueriesEnabled, nbrCommands, individualStatementTimeout);
                          }
  
                          timeoutTask = startQueryTimer(this, individualStatementTimeout);
  
                          updateCounts = new long[nbrCommands];
  
                          for (int i = 0; i < nbrCommands; i++) {
                              updateCounts[i] = -3;
                          }
  
                          SQLException sqlEx = null;
  
                          int commandIndex = 0;
  
                          for (commandIndex = 0; commandIndex < nbrCommands; commandIndex++) {
                              try {
                                  String sql = (String) batchedArgs.get(commandIndex);
                                  updateCounts[commandIndex] = executeUpdateInternal(sql, true, true);
  
                                  if (timeoutTask != null) {
                                      // we need to check the cancel state on each iteration to generate timeout exception if needed
                                      checkCancelTimeout();
                                  }
  
                                  // limit one generated key per OnDuplicateKey statement
                                  getBatchedGeneratedKeys(this.results.getFirstCharOfQuery() == 'I' && containsOnDuplicateKeyInString(sql) ? 1 : 0);
                              } catch (SQLException ex) {
                                  updateCounts[commandIndex] = EXECUTE_FAILED;
  
                                  if (this.continueBatchOnError && !(ex instanceof MySQLTimeoutException) && !(ex instanceof MySQLStatementCancelledException)
                                          && !hasDeadlockOrTimeoutRolledBackTx(ex)) {
                                      sqlEx = ex;
                                  } else {
                                      long[] newUpdateCounts = new long[commandIndex];
  
                                      if (hasDeadlockOrTimeoutRolledBackTx(ex)) {
                                          for (int i = 0; i < newUpdateCounts.length; i++) {
                                              newUpdateCounts[i] = java.sql.Statement.EXECUTE_FAILED;
                                          }
                                      } else {
                                          System.arraycopy(updateCounts, 0, newUpdateCounts, 0, commandIndex);
                                      }
  
                                      sqlEx = ex;
                                      break;
                                      //throw SQLError.createBatchUpdateException(ex, newUpdateCounts, getExceptionInterceptor());
                                  }
                              }
                          }
  
                          if (sqlEx != null) {
                              throw SQLError.createBatchUpdateException(sqlEx, updateCounts, getExceptionInterceptor());
                          }
                      }
  
                      if (timeoutTask != null) {
                          stopQueryTimer(timeoutTask, true, true);
                          timeoutTask = null;
                      }
  
                      return (updateCounts != null) ? updateCounts : new long[0];
                  } finally {
                      this.query.getStatementExecuting().set(false);
                  }
              } finally {
  
                  stopQueryTimer(timeoutTask, false, false);
                  resetCancelledState();
  
                  setTimeoutInMillis(individualStatementTimeout);
  
                  clearBatch();
              }
          }
      }
  
      protected final boolean hasDeadlockOrTimeoutRolledBackTx(SQLException ex) {
          int vendorCode = ex.getErrorCode();
  
          switch (vendorCode) {
              case MysqlErrorNumbers.ER_LOCK_DEADLOCK:
              case MysqlErrorNumbers.ER_LOCK_TABLE_FULL:
                  return true;
              case MysqlErrorNumbers.ER_LOCK_WAIT_TIMEOUT:
                  return false;
              default:
                  return false;
          }
      }
  
      /**
       * Rewrites batch into a single query to send to the server. This method
       * will constrain each batch to be shorter than max_allowed_packet on the
       * server.
       * 
       * @param multiQueriesEnabled
       *            is multi-queries syntax allowed?
       * @param nbrCommands
       *            number of queries in a batch
       * @param individualStatementTimeout
       *            timeout for a single query in a batch
       * 
       * @return update counts in the same manner as executeBatch()
       * @throws SQLException
       *             if a database access error occurs or this method is called on a closed PreparedStatement
       */
      private long[] executeBatchUsingMultiQueries(boolean multiQueriesEnabled, int nbrCommands, int individualStatementTimeout) throws SQLException {
  
          JdbcConnection locallyScopedConn = checkClosed();
  
          synchronized (locallyScopedConn.getConnectionMutex()) {
              if (!multiQueriesEnabled) {
                  this.session.enableMultiQueries();
              }
  
              java.sql.Statement batchStmt = null;
  
              CancelQueryTask timeoutTask = null;
  
              try {
                  long[] updateCounts = new long[nbrCommands];
  
                  for (int i = 0; i < nbrCommands; i++) {
                      updateCounts[i] = JdbcStatement.EXECUTE_FAILED;
                  }
  
                  int commandIndex = 0;
  
                  StringBuilder queryBuf = new StringBuilder();
  
                  batchStmt = locallyScopedConn.createStatement();
                  JdbcStatement jdbcBatchedStmt = (JdbcStatement) batchStmt;
                  getQueryAttributesBindings().runThroughAll(a -> jdbcBatchedStmt.setAttribute(a.getName(), a.getValue()));
  
                  timeoutTask = startQueryTimer((StatementImpl) batchStmt, individualStatementTimeout);
  
                  int counter = 0;
  
                  String connectionEncoding = locallyScopedConn.getPropertySet().getStringProperty(PropertyKey.characterEncoding).getValue();
  
                  int numberOfBytesPerChar = StringUtils.startsWithIgnoreCase(connectionEncoding, "utf") ? 3
                          : (this.session.getServerSession().getCharsetSettings().isMultibyteCharset(connectionEncoding) ? 2 : 1);
  
                  int escapeAdjust = 1;
  
                  batchStmt.setEscapeProcessing(this.doEscapeProcessing);
  
                  if (this.doEscapeProcessing) {
                      escapeAdjust = 2; // We assume packet _could_ grow by this amount, as we're not sure how big statement will end up after escape processing
                  }
  
                  SQLException sqlEx = null;
  
                  int argumentSetsInBatchSoFar = 0;
  
                  for (commandIndex = 0; commandIndex < nbrCommands; commandIndex++) {
                      String nextQuery = (String) this.query.getBatchedArgs().get(commandIndex);
  
                      if (((((queryBuf.length() + nextQuery.length()) * numberOfBytesPerChar) + 1 /* for semicolon */
                              + NativeConstants.HEADER_LENGTH) * escapeAdjust) + 32 > this.maxAllowedPacket.getValue()) {
                          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) {
                      stopQueryTimer(timeoutTask, true, true);
                      timeoutTask = null;
                  }
  
                  if (sqlEx != null) {
                      throw SQLError.createBatchUpdateException(sqlEx, updateCounts, getExceptionInterceptor());
                  }
  
                  return (updateCounts != null) ? updateCounts : new long[0];
              } finally {
                  stopQueryTimer(timeoutTask, false, false);
                  resetCancelledState();
  
                  try {
                      if (batchStmt != null) {
                          batchStmt.close();
                      }
                  } finally {
                      if (!multiQueriesEnabled) {
                          this.session.disableMultiQueries();
                      }
                  }
              }
          }
      }
  
      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());
      }
  
      @Override
      public java.sql.ResultSet executeQuery(String sql) throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              JdbcConnection 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, this.session.getServerSession().getSessionTimeZone(),
                          this.session.getServerSession().getCapabilities().serverSupportsFracSecs(), this.session.getServerSession().isServerTruncatesFracSecs(),
                          getExceptionInterceptor());
                  sql = escapedSqlResult instanceof String ? (String) escapedSqlResult : ((EscapeProcessorResult) escapedSqlResult).escapedSql;
              }
  
              if (!isResultSetProducingQuery(sql)) {
                  throw SQLError.createSQLException(Messages.getString("Statement.57"), MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
              }
  
              CachedResultSetMetaData cachedMetaData = null;
  
              if (useServerFetch()) {
                  this.results = createResultSetUsingServerFetch(sql);
  
                  return this.results;
              }
  
              CancelQueryTask timeoutTask = null;
  
              String oldDb = null;
  
              try {
                  timeoutTask = startQueryTimer(this, getTimeoutInMillis());
  
                  if (!locallyScopedConn.getDatabase().equals(getCurrentDatabase())) {
                      oldDb = locallyScopedConn.getDatabase();
                      locallyScopedConn.setDatabase(getCurrentDatabase());
                  }
  
                  //
                  // Check if we have cached metadata for this query...
                  //
                  if (locallyScopedConn.getPropertySet().getBooleanProperty(PropertyKey.cacheResultSetMetadata).getValue()) {
                      cachedMetaData = locallyScopedConn.getCachedMetaData(sql);
                  }
  
                  locallyScopedConn.setSessionMaxRows(this.maxRows);
  
                  statementBegins();
  
                  this.results = ((NativeSession) locallyScopedConn.getSession()).execSQL(this, sql, this.maxRows, null, createStreamingResultSet(),
                          getResultSetFactory(), cachedMetaData, false);
  
                  if (timeoutTask != null) {
                      stopQueryTimer(timeoutTask, true, true);
                      timeoutTask = null;
                  }
  
              } catch (CJTimeoutException | OperationCancelledException e) {
                  throw SQLExceptionsMapping.translateException(e, this.exceptionInterceptor);
  
              } finally {
                  this.query.getStatementExecuting().set(false);
  
                  stopQueryTimer(timeoutTask, false, false);
  
                  if (oldDb != null) {
                      locallyScopedConn.setDatabase(oldDb);
                  }
              }
  
              this.lastInsertId = this.results.getUpdateID();
  
              if (cachedMetaData != null) {
                  locallyScopedConn.initializeResultsMetadataFromCache(sql, cachedMetaData, this.results);
              } else {
                  if (this.connection.getPropertySet().getBooleanProperty(PropertyKey.cacheResultSetMetadata).getValue()) {
                      locallyScopedConn.initializeResultsMetadataFromCache(sql, null /* will be created */, this.results);
                  }
              }
  
              return this.results;
          }
      }
  
      protected void doPingInstead() throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              if (this.pingTarget != null) {
                  try {
                      this.pingTarget.doPing();
                  } catch (SQLException e) {
                      throw e;
                  } catch (Exception e) {
                      throw SQLError.createSQLException(e.getMessage(), MysqlErrorNumbers.SQL_STATE_COMMUNICATION_LINK_FAILURE, e, getExceptionInterceptor());
                  }
              } else {
                  this.connection.ping();
              }
  
              ResultSetInternalMethods fakeSelectOneResultSet = generatePingResultSet();
              this.results = fakeSelectOneResultSet;
          }
      }
  
      protected ResultSetInternalMethods generatePingResultSet() throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              String encoding = this.session.getServerSession().getCharsetSettings().getMetadataEncoding();
              int collationIndex = this.session.getServerSession().getCharsetSettings().getMetadataCollationIndex();
              Field[] fields = { new Field(null, "1", collationIndex, encoding, MysqlType.BIGINT, 1) };
              ArrayList<Row> rows = new ArrayList<>();
              byte[] colVal = new byte[] { (byte) '1' };
  
              rows.add(new ByteArrayRow(new byte[][] { colVal }, getExceptionInterceptor()));
  
              return this.resultSetFactory.createFromResultsetRows(ResultSet.CONCUR_READ_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE,
                      new ResultsetRowsStatic(rows, new DefaultColumnDefinition(fields)));
          }
      }
  
      public void executeSimpleNonQuery(JdbcConnection c, String nonQuery) throws SQLException {
          synchronized (c.getConnectionMutex()) {
              ((NativeSession) c.getSession()).<ResultSetImpl>execSQL(this, nonQuery, -1, null, false, getResultSetFactory(), null, false).close();
          }
      }
  
      @Override
      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()) {
              JdbcConnection locallyScopedConn = this.connection;
  
              checkNullOrEmptyQuery(sql);
  
              resetCancelledState();
  
              char firstStatementChar = QueryInfo.firstCharOfStatementUc(sql, this.session.getServerSession().isNoBackslashEscapesSet());
              if (!isNonResultSetProducingQuery(sql)) {
                  throw SQLError.createSQLException(Messages.getString("Statement.46"), "01S03", getExceptionInterceptor());
              }
  
              this.retrieveGeneratedKeys = returnGeneratedKeys;
  
              this.lastQueryIsOnDupKeyUpdate = returnGeneratedKeys && firstStatementChar == 'I' && containsOnDuplicateKeyInString(sql);
  
              ResultSetInternalMethods rs = null;
  
              if (this.doEscapeProcessing) {
                  Object escapedSqlResult = EscapeProcessor.escapeSQL(sql, this.session.getServerSession().getSessionTimeZone(),
                          this.session.getServerSession().getCapabilities().serverSupportsFracSecs(), this.session.getServerSession().isServerTruncatesFracSecs(),
                          getExceptionInterceptor());
                  sql = escapedSqlResult instanceof String ? (String) escapedSqlResult : ((EscapeProcessorResult) escapedSqlResult).escapedSql;
              }
  
              if (locallyScopedConn.isReadOnly(false)) {
                  throw SQLError.createSQLException(Messages.getString("Statement.42") + Messages.getString("Statement.43"),
                          MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
              }
  
              implicitlyCloseAllOpenResults();
  
              // The checking and changing of databases must happen in sequence, so synchronize on the same mutex that _conn is using
  
              CancelQueryTask timeoutTask = null;
  
              String oldDb = null;
  
              try {
                  timeoutTask = startQueryTimer(this, getTimeoutInMillis());
  
                  if (!locallyScopedConn.getDatabase().equals(getCurrentDatabase())) {
                      oldDb = locallyScopedConn.getDatabase();
                      locallyScopedConn.setDatabase(getCurrentDatabase());
                  }
  
                  //
                  // Only apply max_rows to selects
                  //
                  locallyScopedConn.setSessionMaxRows(-1);
  
                  statementBegins();
  
                  // null database: force read of field info on DML
                  rs = ((NativeSession) locallyScopedConn.getSession()).execSQL(this, sql, -1, null, false, getResultSetFactory(), null, isBatch);
  
                  if (timeoutTask != null) {
                      stopQueryTimer(timeoutTask, true, true);
                      timeoutTask = null;
                  }
  
              } catch (CJTimeoutException | OperationCancelledException e) {
                  throw SQLExceptionsMapping.translateException(e, this.exceptionInterceptor);
  
              } finally {
                  stopQueryTimer(timeoutTask, false, false);
  
                  if (oldDb != null) {
                      locallyScopedConn.setDatabase(oldDb);
                  }
  
                  if (!isBatch) {
                      this.query.getStatementExecuting().set(false);
                  }
              }
  
              this.results = rs;
  
              rs.setFirstCharOfQuery(firstStatementChar);
  
              this.updateCount = rs.getUpdateCount();
  
              this.lastInsertId = rs.getUpdateID();
  
              return this.updateCount;
          }
      }
  
      @Override
      public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
          return Util.truncateAndConvertToInt(executeLargeUpdate(sql, autoGeneratedKeys));
      }
  
      @Override
      public int executeUpdate(String sql, int[] columnIndexes) throws SQLException {
          return Util.truncateAndConvertToInt(executeLargeUpdate(sql, columnIndexes));
      }
  
      @Override
      public int executeUpdate(String sql, String[] columnNames) throws SQLException {
          return Util.truncateAndConvertToInt(executeLargeUpdate(sql, columnNames));
      }
  
      @Override
      public java.sql.Connection getConnection() throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              return this.connection;
          }
      }
  
      @Override
      public int getFetchDirection() throws SQLException {
          return java.sql.ResultSet.FETCH_FORWARD;
      }
  
      @Override
      public int getFetchSize() throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              return this.query.getResultFetchSize();
          }
      }
  
      @Override
      public java.sql.ResultSet getGeneratedKeys() throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              if (!this.retrieveGeneratedKeys) {
                  throw SQLError.createSQLException(Messages.getString("Statement.GeneratedKeysNotRequested"), MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT,
                          getExceptionInterceptor());
              }
  
              if (this.batchedGeneratedKeys == null) {
                  if (this.lastQueryIsOnDupKeyUpdate) {
                      return this.generatedKeysResults = getGeneratedKeysInternal(1);
                  }
                  return this.generatedKeysResults = getGeneratedKeysInternal();
              }
  
              String encoding = this.session.getServerSession().getCharsetSettings().getMetadataEncoding();
              int collationIndex = this.session.getServerSession().getCharsetSettings().getMetadataCollationIndex();
              Field[] fields = new Field[1];
              fields[0] = new Field("", "GENERATED_KEY", collationIndex, encoding, MysqlType.BIGINT_UNSIGNED, 20);
  
              this.generatedKeysResults = this.resultSetFactory.createFromResultsetRows(ResultSet.CONCUR_READ_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE,
                      new ResultsetRowsStatic(this.batchedGeneratedKeys, new DefaultColumnDefinition(fields)));
  
              return this.generatedKeysResults;
          }
      }
  
      /*
       * 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()) {
              String encoding = this.session.getServerSession().getCharsetSettings().getMetadataEncoding();
              int collationIndex = this.session.getServerSession().getCharsetSettings().getMetadataCollationIndex();
              Field[] fields = new Field[1];
              fields[0] = new Field("", "GENERATED_KEY", collationIndex, encoding, MysqlType.BIGINT_UNSIGNED, 20);
  
              ArrayList<Row> rowSet = new ArrayList<>();
  
              long beginAt = getLastInsertID();
  
              if (this.results != null) {
                  String serverInfo = this.results.getServerInfo();
  
                  //
                  // Only parse server info messages for 'REPLACE' queries
                  //
                  if ((numKeys > 0) && (this.results.getFirstCharOfQuery() == 'R') && (serverInfo != null) && (serverInfo.length() > 0)) {
                      numKeys = getRecordCountFromInfo(serverInfo);
                  }
  
                  if ((beginAt != 0 /* BIGINT UNSIGNED can wrap the protocol representation */) && (numKeys > 0)) {
                      for (int i = 0; i < numKeys; i++) {
                          byte[][] row = new byte[1][];
                          if (beginAt > 0) {
                              row[0] = StringUtils.getBytes(Long.toString(beginAt));
                          } else {
                              byte[] asBytes = new byte[8];
                              asBytes[7] = (byte) (beginAt & 0xff);
                              asBytes[6] = (byte) (beginAt >>> 8);
                              asBytes[5] = (byte) (beginAt >>> 16);
                              asBytes[4] = (byte) (beginAt >>> 24);
                              asBytes[3] = (byte) (beginAt >>> 32);
                              asBytes[2] = (byte) (beginAt >>> 40);
                              asBytes[1] = (byte) (beginAt >>> 48);
                              asBytes[0] = (byte) (beginAt >>> 56);
  
                              BigInteger val = new BigInteger(1, asBytes);
  
                              row[0] = val.toString().getBytes();
                          }
                          rowSet.add(new ByteArrayRow(row, getExceptionInterceptor()));
                          beginAt += this.connection.getAutoIncrementIncrement();
                      }
                  }
              }
  
              ResultSetImpl gkRs = this.resultSetFactory.createFromResultsetRows(ResultSet.CONCUR_READ_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE,
                      new ResultsetRowsStatic(rowSet, new DefaultColumnDefinition(fields)));
  
              return gkRs;
          }
      }
  
      /**
       * 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() {
          synchronized (checkClosed().getConnectionMutex()) {
              return this.lastInsertId;
          }
      }
  
      /**
       * 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 returns 64-bit values for update counts
       * </p>
       * 
       * @return the current update count.
       */
      public long getLongUpdateCount() {
          synchronized (checkClosed().getConnectionMutex()) {
              if (this.results == null) {
                  return -1;
              }
  
              if (this.results.hasRows()) {
                  return -1;
              }
  
              return this.updateCount;
          }
      }
  
      @Override
      public int getMaxFieldSize() throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              return this.maxFieldSize;
          }
      }
  
      @Override
      public int getMaxRows() throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              if (this.maxRows <= 0) {
                  return 0;
              }
  
              return this.maxRows;
          }
      }
  
      @Override
      public boolean getMoreResults() throws SQLException {
          return getMoreResults(CLOSE_CURRENT_RESULT);
      }
  
      @Override
      public boolean getMoreResults(int current) throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              if (this.results == null) {
                  return false;
              }
  
              boolean streamingMode = createStreamingResultSet();
  
              if (streamingMode) {
                  if (this.results.hasRows()) {
                      while (this.results.next()) {
                          // need to drain remaining rows to get to server status which tells us whether more results actually exist or not
                      }
                  }
              }
  
              ResultSetInternalMethods nextResultSet = (ResultSetInternalMethods) this.results.getNextResultset();
  
              switch (current) {
                  case java.sql.Statement.CLOSE_CURRENT_RESULT:
  
                      if (this.results != null) {
                          if (!(streamingMode || this.dontTrackOpenResources.getValue())) {
                              this.results.realClose(false);
                          }
  
                          this.results.clearNextResultset();
                      }
  
                      break;
  
                  case java.sql.Statement.CLOSE_ALL_RESULTS:
  
                      if (this.results != null) {
                          if (!(streamingMode || this.dontTrackOpenResources.getValue())) {
                              this.results.realClose(false);
                          }
  
                          this.results.clearNextResultset();
                      }
  
                      closeAllOpenResults();
  
                      break;
  
                  case java.sql.Statement.KEEP_CURRENT_RESULT:
                      if (!this.dontTrackOpenResources.getValue()) {
                          this.openResults.add(this.results);
                      }
  
                      this.results.clearNextResultset(); // nobody besides us should
                      // ever need this value...
                      break;
  
                  default:
                      throw SQLError.createSQLException(Messages.getString("Statement.19"), MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT,
                              getExceptionInterceptor());
              }
  
              this.results = nextResultSet;
  
              if (this.results == null) {
                  this.updateCount = -1;
                  this.lastInsertId = -1;
              } else if (this.results.hasRows()) {
                  this.updateCount = -1;
                  this.lastInsertId = -1;
              } else {
                  this.updateCount = this.results.getUpdateCount();
                  this.lastInsertId = this.results.getUpdateID();
              }
  
              boolean moreResults = (this.results != null) && this.results.hasRows();
              if (!moreResults) {
                  checkAndPerformCloseOnCompletionAction();
              }
              return moreResults;
          }
      }
  
      @Override
      public int getQueryTimeout() throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              return getTimeoutInMillis() / 1000;
          }
      }
  
      /**
       * Parses actual record count from 'info' message
       * 
       * @param serverInfo
       *            server info message
       * @return records count
       */
      private long getRecordCountFromInfo(String serverInfo) {
          StringBuilder recordsBuf = new StringBuilder();
          long recordsCount = 0;
          long duplicatesCount = 0;
  
          char c = (char) 0;
  
          int length = serverInfo.length();
          int i = 0;
  
          for (; i < length; i++) {
              c = serverInfo.charAt(i);
  
              if (Character.isDigit(c)) {
                  break;
              }
          }
  
          recordsBuf.append(c);
          i++;
  
          for (; i < length; i++) {
              c = serverInfo.charAt(i);
  
              if (!Character.isDigit(c)) {
                  break;
              }
  
              recordsBuf.append(c);
          }
  
          recordsCount = Long.parseLong(recordsBuf.toString());
  
          StringBuilder duplicatesBuf = new StringBuilder();
  
          for (; i < length; i++) {
              c = serverInfo.charAt(i);
  
              if (Character.isDigit(c)) {
                  break;
              }
          }
  
          duplicatesBuf.append(c);
          i++;
  
          for (; i < length; i++) {
              c = serverInfo.charAt(i);
  
              if (!Character.isDigit(c)) {
                  break;
              }
  
              duplicatesBuf.append(c);
          }
  
          duplicatesCount = Long.parseLong(duplicatesBuf.toString());
  
          return recordsCount - duplicatesCount;
      }
  
      @Override
      public java.sql.ResultSet getResultSet() throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              return ((this.results != null) && this.results.hasRows()) ? (java.sql.ResultSet) this.results : null;
          }
      }
  
      @Override
      public int getResultSetConcurrency() throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              return this.resultSetConcurrency;
          }
      }
  
      @Override
      public int getResultSetHoldability() throws SQLException {
          return java.sql.ResultSet.HOLD_CURSORS_OVER_COMMIT;
      }
  
      @Override
      public ResultSetInternalMethods getResultSetInternal() {
          try {
              synchronized (checkClosed().getConnectionMutex()) {
                  return this.results;
              }
          } catch (StatementIsClosedException e) {
              return this.results; // you end up with the same thing as before, you'll get exception when actually trying to use it
          }
      }
  
      @Override
      public int getResultSetType() throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              return this.query.getResultType().getIntValue();
          }
      }
  
      @Override
      public int getUpdateCount() throws SQLException {
          return Util.truncateAndConvertToInt(getLargeUpdateCount());
      }
  
      @Override
      public java.sql.SQLWarning getWarnings() throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
  
              if (isClearWarningsCalled()) {
                  return null;
              }
  
              SQLWarning pendingWarningsFromServer = this.session.getProtocol().convertShowWarningsToSQLWarnings(false);
  
              if (this.warningChain != null) {
                  this.warningChain.setNextWarning(pendingWarningsFromServer);
              } else {
                  this.warningChain = pendingWarningsFromServer;
              }
  
              return this.warningChain;
          }
      }
  
      /**
       * Closes this statement, and frees resources.
       * 
       * @param calledExplicitly
       *            was this called from close()?
       * @param closeOpenResults
       *            should open result sets be closed?
       * 
       * @throws SQLException
       *             if an error occurs
       */
      protected void realClose(boolean calledExplicitly, boolean closeOpenResults) throws SQLException {
          JdbcConnection locallyScopedConn = this.connection;
  
          if (locallyScopedConn == null || this.isClosed) {
              return; // already closed
          }
  
          // do it ASAP to reduce the chance of calling this method concurrently from ConnectionImpl.closeAllOpenStatements()
          if (!this.dontTrackOpenResources.getValue()) {
              locallyScopedConn.unregisterStatement(this);
          }
  
          if (this.useUsageAdvisor) {
              if (!calledExplicitly) {
                  this.session.getProfilerEventHandler().processEvent(ProfilerEvent.TYPE_USAGE, this.session, this, null, 0, new Throwable(),
                          Messages.getString("Statement.63"));
              }
          }
  
          if (closeOpenResults) {
              closeOpenResults = !(this.holdResultsOpenOverClose || this.dontTrackOpenResources.getValue());
          }
  
          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();
          }
  
          clearAttributes();
  
          this.isClosed = true;
  
          closeQuery();
  
          this.results = null;
          this.generatedKeysResults = null;
          this.connection = null;
          this.session = null;
          this.warningChain = null;
          this.openResults = null;
          this.batchedGeneratedKeys = null;
          this.pingTarget = null;
          this.resultSetFactory = null;
      }
  
      @Override
      public void setCursorName(String name) throws SQLException {
          // No-op
      }
  
      @Override
      public void setEscapeProcessing(boolean enable) throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              this.doEscapeProcessing = enable;
          }
      }
  
      @Override
      public void setFetchDirection(int direction) throws SQLException {
          switch (direction) {
              case java.sql.ResultSet.FETCH_FORWARD:
              case java.sql.ResultSet.FETCH_REVERSE:
              case java.sql.ResultSet.FETCH_UNKNOWN:
                  break;
  
              default:
                  throw SQLError.createSQLException(Messages.getString("Statement.5"), MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
          }
      }
  
      @Override
      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"), MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
              }
  
              this.query.setResultFetchSize(rows);
          }
      }
  
      @Override
      public void setHoldResultsOpenOverClose(boolean holdResultsOpenOverClose) {
          try {
              synchronized (checkClosed().getConnectionMutex()) {
                  this.holdResultsOpenOverClose = holdResultsOpenOverClose;
              }
          } catch (StatementIsClosedException e) {
              // FIXME: can't break interface at this point
          }
      }
  
      @Override
      public void setMaxFieldSize(int max) throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              if (max < 0) {
                  throw SQLError.createSQLException(Messages.getString("Statement.11"), MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
              }
  
              int maxBuf = this.maxAllowedPacket.getValue();
  
              if (max > maxBuf) {
                  throw SQLError.createSQLException(Messages.getString("Statement.13", new Object[] { Long.valueOf(maxBuf) }),
                          MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
              }
  
              this.maxFieldSize = max;
          }
      }
  
      @Override
      public void setMaxRows(int max) throws SQLException {
          setLargeMaxRows(max);
      }
  
      @Override
      public void setQueryTimeout(int seconds) throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              if (seconds < 0) {
                  throw SQLError.createSQLException(Messages.getString("Statement.21"), MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
              }
  
              setTimeoutInMillis(seconds * 1000);
          }
      }
  
      /**
       * Sets the concurrency for result sets generated by this statement
       * 
       * @param concurrencyFlag
       *            concurrency flag
       * @throws SQLException
       *             if a database access error occurs or this method is called on a closed PreparedStatement
       */
      void setResultSetConcurrency(int concurrencyFlag) throws SQLException {
          try {
              synchronized (checkClosed().getConnectionMutex()) {
                  this.resultSetConcurrency = concurrencyFlag;
                  // updating resultset factory because concurrency is cached there
                  this.resultSetFactory = new ResultSetFactory(this.connection, this);
              }
          } catch (StatementIsClosedException e) {
              // FIXME: Can't break interface atm, we'll get the exception later when you try and do something useful with a closed statement...
          }
      }
  
      /**
       * Sets the result set type for result sets generated by this statement
       * 
       * @param typeFlag
       *            {@link com.mysql.cj.protocol.Resultset.Type}
       * @throws SQLException
       *             if a database access error occurs or this method is called on a closed PreparedStatement
       */
      void setResultSetType(Resultset.Type typeFlag) throws SQLException {
          try {
              synchronized (checkClosed().getConnectionMutex()) {
                  this.query.setResultType(typeFlag);
                  // updating resultset factory because type is cached there
                  this.resultSetFactory = new ResultSetFactory(this.connection, this);
              }
          } catch (StatementIsClosedException e) {
              // FIXME: Can't break interface atm, we'll get the exception later when you try and do something useful with a closed statement...
          }
      }
  
      void setResultSetType(int typeFlag) throws SQLException {
          this.query.setResultType(Type.fromValue(typeFlag, Type.FORWARD_ONLY));
      }
  
      protected void getBatchedGeneratedKeys(java.sql.Statement batchedStatement) throws SQLException {
          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 {
                      rs = maxKeys == 0 ? getGeneratedKeysInternal() : 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.session.getPropertySet().getBooleanProperty(PropertyKey.useCursorFetch).getValue() && this.query.getResultFetchSize() > 0
                      && this.query.getResultType() == Type.FORWARD_ONLY;
          }
      }
  
      @Override
      public boolean isClosed() throws SQLException {
          JdbcConnection locallyScopedConn = this.connection;
          if (locallyScopedConn == null) {
              return true;
          }
          synchronized (locallyScopedConn.getConnectionMutex()) {
              return this.isClosed;
          }
      }
  
      private boolean isPoolable = false;
  
      @Override
      public boolean isPoolable() throws SQLException {
          checkClosed();
          return this.isPoolable;
      }
  
      @Override
      public void setPoolable(boolean poolable) throws SQLException {
          checkClosed();
          this.isPoolable = poolable;
      }
  
      @Override
      public boolean isWrapperFor(Class<?> iface) throws SQLException {
          checkClosed();
  
          // This works for classes that aren't actually wrapping anything
          return iface.isInstance(this);
      }
  
      @Override
      public <T> T unwrap(Class<T> iface) throws SQLException {
          try {
              // This works for classes that aren't actually wrapping anything
              return iface.cast(this);
          } catch (ClassCastException cce) {
              throw SQLError.createSQLException(Messages.getString("Common.UnableToUnwrap", new Object[] { iface.toString() }),
                      MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
          }
      }
  
      @Override
      public InputStream getLocalInfileInputStream() {
          return this.session.getLocalInfileInputStream();
      }
  
      @Override
      public void setLocalInfileInputStream(InputStream stream) {
          this.session.setLocalInfileInputStream(stream);
      }
  
      @Override
      public void setPingTarget(PingTarget pingTarget) {
          this.pingTarget = pingTarget;
      }
  
      @Override
      public ExceptionInterceptor getExceptionInterceptor() {
          return this.exceptionInterceptor;
      }
  
      protected boolean containsOnDuplicateKeyInString(String sql) {
          return (!this.dontCheckOnDuplicateKeyUpdateInSQL || this.rewriteBatchedStatements.getValue())
                  && QueryInfo.containsOnDuplicateKeyUpdateClause(sql, this.session.getServerSession().isNoBackslashEscapesSet());
      }
  
      private boolean closeOnCompletion = false;
  
      @Override
      public void closeOnCompletion() throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              this.closeOnCompletion = true;
          }
      }
  
      @Override
      public boolean isCloseOnCompletion() throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              return this.closeOnCompletion;
          }
      }
  
      @Override
      public long[] executeLargeBatch() throws SQLException {
          return executeBatchInternal();
      }
  
      @Override
      public long executeLargeUpdate(String sql) throws SQLException {
          return executeUpdateInternal(sql, false, false);
      }
  
      @Override
      public long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
          return executeUpdateInternal(sql, false, autoGeneratedKeys == java.sql.Statement.RETURN_GENERATED_KEYS);
      }
  
      @Override
      public long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLException {
          return executeUpdateInternal(sql, false, columnIndexes != null && columnIndexes.length > 0);
      }
  
      @Override
      public long executeLargeUpdate(String sql, String[] columnNames) throws SQLException {
          return executeUpdateInternal(sql, false, columnNames != null && columnNames.length > 0);
      }
  
      @Override
      public long getLargeMaxRows() throws SQLException {
          // Max rows is limited by MySQLDefs.MAX_ROWS anyway...
          return getMaxRows();
      }
  
      @Override
      public long getLargeUpdateCount() throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              if (this.results == null) {
                  return -1;
              }
  
              if (this.results.hasRows()) {
                  return -1;
              }
  
              return this.results.getUpdateCount();
          }
      }
  
      @Override
      public void setLargeMaxRows(long max) throws SQLException {
          synchronized (checkClosed().getConnectionMutex()) {
              if ((max > MAX_ROWS) || (max < 0)) {
                  throw SQLError.createSQLException(Messages.getString("Statement.15") + max + " > " + MAX_ROWS + ".",
                          MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
              }
  
              if (max == 0) {
                  max = -1;
              }
  
              this.maxRows = (int) max;
          }
      }
  
      @Override
      public String getCurrentDatabase() {
          return this.query.getCurrentDatabase();
      }
  
      public long getServerStatementId() {
          throw ExceptionFactory.createException(CJOperationNotSupportedException.class, Messages.getString("Statement.65"));
      }
  
      @Override
      @SuppressWarnings("unchecked")
      public <T extends Resultset, M extends Message> ProtocolEntityFactory<T, M> getResultSetFactory() {
          return (ProtocolEntityFactory<T, M>) this.resultSetFactory;
      }
  
      @Override
      public int getId() {
          return this.query.getId();
      }
  
      @Override
      public void setCancelStatus(CancelStatus cs) {
          this.query.setCancelStatus(cs);
      }
  
      @Override
      public void checkCancelTimeout() {
          this.query.checkCancelTimeout();
      }
  
      @Override
      public Session getSession() {
          return this.session;
      }
  
      @Override
      public Object getCancelTimeoutMutex() {
          return this.query.getCancelTimeoutMutex();
      }
  
      @Override
      public void closeQuery() {
          if (this.query != null) {
              this.query.closeQuery();
          }
      }
  
      @Override
      public int getResultFetchSize() {
          return this.query.getResultFetchSize();
      }
  
      @Override
      public void setResultFetchSize(int fetchSize) {
          this.query.setResultFetchSize(fetchSize);
      }
  
      @Override
      public Resultset.Type getResultType() {
          return this.query.getResultType();
      }
  
      @Override
      public void setResultType(Resultset.Type resultSetType) {
          this.query.setResultType(resultSetType);
      }
  
      @Override
      public int getTimeoutInMillis() {
          return this.query.getTimeoutInMillis();
      }
  
      @Override
      public void setTimeoutInMillis(int timeoutInMillis) {
          this.query.setTimeoutInMillis(timeoutInMillis);
      }
  
      @Override
      public long getExecuteTime() {
          return this.query.getExecuteTime();
      }
  
      @Override
      public void setExecuteTime(long executeTime) {
          this.query.setExecuteTime(executeTime);
      }
  
      @Override
      public AtomicBoolean getStatementExecuting() {
          return this.query.getStatementExecuting();
      }
  
      @Override
      public void setCurrentDatabase(String currentDb) {
          this.query.setCurrentDatabase(currentDb);
      }
  
      @Override
      public boolean isClearWarningsCalled() {
          return this.query.isClearWarningsCalled();
      }
  
      @Override
      public void setClearWarningsCalled(boolean clearWarningsCalled) {
          this.query.setClearWarningsCalled(clearWarningsCalled);
      }
  
      @Override
      public Query getQuery() {
          return this.query;
      }
  
      @Override
      public QueryAttributesBindings getQueryAttributesBindings() {
          return this.query.getQueryAttributesBindings();
      }
  
      @Override
      public void setAttribute(String name, Object value) {
          getQueryAttributesBindings().setAttribute(name, value);
      }
  
      @Override
      public void clearAttributes() {
          QueryAttributesBindings qab = getQueryAttributesBindings();
          if (qab != null) {
              qab.clearAttributes();
          }
      }
  }
  