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.Serializable;
  import java.lang.ref.WeakReference;
  import java.lang.reflect.InvocationHandler;
  import java.sql.Blob;
  import java.sql.Clob;
  import java.sql.DatabaseMetaData;
  import java.sql.DriverManager;
  import java.sql.NClob;
  import java.sql.ResultSet;
  import java.sql.SQLClientInfoException;
  import java.sql.SQLException;
  import java.sql.SQLPermission;
  import java.sql.SQLWarning;
  import java.sql.SQLXML;
  import java.sql.Savepoint;
  import java.sql.Struct;
  import java.util.HashMap;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  import java.util.Properties;
  import java.util.Random;
  import java.util.Stack;
  import java.util.concurrent.CopyOnWriteArrayList;
  import java.util.concurrent.Executor;
  import java.util.stream.Collectors;
  
  import com.mysql.cj.CacheAdapter;
  import com.mysql.cj.CacheAdapterFactory;
  import com.mysql.cj.LicenseConfiguration;
  import com.mysql.cj.Messages;
  import com.mysql.cj.NativeSession;
  import com.mysql.cj.NoSubInterceptorWrapper;
  import com.mysql.cj.PreparedQuery;
  import com.mysql.cj.QueryInfo;
  import com.mysql.cj.ServerVersion;
  import com.mysql.cj.Session.SessionEventListener;
  import com.mysql.cj.conf.HostInfo;
  import com.mysql.cj.conf.PropertyDefinitions.DatabaseTerm;
  import com.mysql.cj.conf.PropertyKey;
  import com.mysql.cj.conf.RuntimeProperty;
  import com.mysql.cj.exceptions.CJCommunicationsException;
  import com.mysql.cj.exceptions.CJException;
  import com.mysql.cj.exceptions.ExceptionFactory;
  import com.mysql.cj.exceptions.ExceptionInterceptor;
  import com.mysql.cj.exceptions.ExceptionInterceptorChain;
  import com.mysql.cj.exceptions.MysqlErrorNumbers;
  import com.mysql.cj.exceptions.PasswordExpiredException;
  import com.mysql.cj.exceptions.UnableToConnectException;
  import com.mysql.cj.interceptors.QueryInterceptor;
  import com.mysql.cj.jdbc.exceptions.SQLError;
  import com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping;
  import com.mysql.cj.jdbc.ha.MultiHostMySQLConnection;
  import com.mysql.cj.jdbc.interceptors.ConnectionLifecycleInterceptor;
  import com.mysql.cj.jdbc.result.CachedResultSetMetaData;
  import com.mysql.cj.jdbc.result.CachedResultSetMetaDataImpl;
  import com.mysql.cj.jdbc.result.ResultSetFactory;
  import com.mysql.cj.jdbc.result.ResultSetInternalMethods;
  import com.mysql.cj.jdbc.result.UpdatableResultSet;
  import com.mysql.cj.log.ProfilerEvent;
  import com.mysql.cj.log.StandardLogger;
  import com.mysql.cj.protocol.ServerSessionStateController;
  import com.mysql.cj.protocol.SocksProxySocketFactory;
  import com.mysql.cj.util.LRUCache;
  import com.mysql.cj.util.StringUtils;
  import com.mysql.cj.util.Util;
  
  /**
   * A Connection represents a session with a specific database. Within the context of a Connection, SQL statements are executed and results are returned.
   * 
   * <P>
   * A Connection's database is able to provide information describing its tables, its supported SQL grammar, its stored procedures, the capabilities of this
   * connection, etc. This information is obtained with the getMetaData method.
   * </p>
   */
  public class ConnectionImpl implements JdbcConnection, SessionEventListener, Serializable {
  
      private static final long serialVersionUID = 4009476458425101761L;
  
      private static final SQLPermission SET_NETWORK_TIMEOUT_PERM = new SQLPermission("setNetworkTimeout");
  
      private static final SQLPermission ABORT_PERM = new SQLPermission("abort");
  
      @Override
      public String getHost() {
          return this.session.getHostInfo().getHost();
      }
  
      private JdbcConnection parentProxy = null;
      private JdbcConnection topProxy = null;
      private InvocationHandler realProxy = null;
  
      @Override
      public boolean isProxySet() {
          return this.topProxy != null;
      }
  
      @Override
      public void setProxy(JdbcConnection proxy) {
          if (this.parentProxy == null) { // Only set this once.
              this.parentProxy = proxy;
          }
          this.topProxy = proxy;
          this.realProxy = this.topProxy instanceof MultiHostMySQLConnection ? ((MultiHostMySQLConnection) proxy).getThisAsProxy() : null;
      }
  
      // this connection has to be proxied when using multi-host settings so that statements get routed to the right physical connection
      // (works as "logical" connection)
      private JdbcConnection getProxy() {
          return (this.topProxy != null) ? this.topProxy : (JdbcConnection) this;
      }
  
      @Override
      public JdbcConnection getMultiHostSafeProxy() {
          return this.getProxy();
      }
  
      @Override
      public JdbcConnection getMultiHostParentProxy() {
          return this.parentProxy;
      }
  
      @Override
      public JdbcConnection getActiveMySQLConnection() {
          return this;
      }
  
      @Override
      public Object getConnectionMutex() {
          return (this.realProxy != null) ? this.realProxy : getProxy();
      }
  
      /**
       * Used as a key for caching callable statements which (may) depend on
       * current database...In 5.0.x, they don't (currently), but stored procedure
       * names soon will, so current database is a (hidden) component of the name.
       */
      static class CompoundCacheKey {
          final String componentOne;
  
          final String componentTwo;
  
          final int hashCode;
  
          CompoundCacheKey(String partOne, String partTwo) {
              this.componentOne = partOne;
              this.componentTwo = partTwo;
  
              int hc = 17;
              hc = 31 * hc + (this.componentOne != null ? this.componentOne.hashCode() : 0);
              hc = 31 * hc + (this.componentTwo != null ? this.componentTwo.hashCode() : 0);
              this.hashCode = hc;
          }
  
          @Override
          public boolean equals(Object obj) {
              if (this == obj) {
                  return true;
              }
              if (obj != null && CompoundCacheKey.class.isAssignableFrom(obj.getClass())) {
                  CompoundCacheKey another = (CompoundCacheKey) obj;
                  if (this.componentOne == null ? another.componentOne == null : this.componentOne.equals(another.componentOne)) {
                      return this.componentTwo == null ? another.componentTwo == null : this.componentTwo.equals(another.componentTwo);
                  }
              }
              return false;
          }
  
          @Override
          public int hashCode() {
              return this.hashCode;
          }
      }
  
      /** Default logger class name */
      protected static final String DEFAULT_LOGGER_CLASS = StandardLogger.class.getName();
  
      /**
       * Map mysql transaction isolation level name to
       * java.sql.Connection.TRANSACTION_XXX
       */
      private static Map<String, Integer> mapTransIsolationNameToValue = null;
      static {
          mapTransIsolationNameToValue = new HashMap<>(8);
          mapTransIsolationNameToValue.put("READ-UNCOMMITED", TRANSACTION_READ_UNCOMMITTED);
          mapTransIsolationNameToValue.put("READ-UNCOMMITTED", TRANSACTION_READ_UNCOMMITTED);
          mapTransIsolationNameToValue.put("READ-COMMITTED", TRANSACTION_READ_COMMITTED);
          mapTransIsolationNameToValue.put("REPEATABLE-READ", TRANSACTION_REPEATABLE_READ);
          mapTransIsolationNameToValue.put("SERIALIZABLE", TRANSACTION_SERIALIZABLE);
      }
  
      protected static Map<?, ?> roundRobinStatsMap;
  
      private List<ConnectionLifecycleInterceptor> connectionLifecycleInterceptors;
  
      private static final int DEFAULT_RESULT_SET_TYPE = ResultSet.TYPE_FORWARD_ONLY;
  
      private static final int DEFAULT_RESULT_SET_CONCURRENCY = ResultSet.CONCUR_READ_ONLY;
  
      /**
       * Creates a connection instance.
       * 
       * @param hostInfo
       *            {@link HostInfo} instance
       * @return new {@link ConnectionImpl} instance
       * @throws SQLException
       *             if a database access error occurs
       */
      public static JdbcConnection getInstance(HostInfo hostInfo) throws SQLException {
          return new ConnectionImpl(hostInfo);
      }
  
      private static final Random random = new Random();
  
      /**
       * @param url
       *            connection URL
       * @param hostList
       *            hosts list
       * @return index in a host list
       */
      protected static synchronized int getNextRoundRobinHostIndex(String url, List<?> hostList) {
          // we really do "random" here, because you don't get even distribution when this is coupled with connection pools
  
          int indexRange = hostList.size();
  
          int index = random.nextInt(indexRange);
  
          return index;
      }
  
      private static boolean nullSafeCompare(String s1, String s2) {
          if (s1 == null && s2 == null) {
              return true;
          }
  
          if (s1 == null && s2 != null) {
              return false;
          }
  
          return s1 != null && s1.equals(s2);
      }
  
      /** A cache of SQL to parsed prepared statement parameters. */
      private CacheAdapter<String, QueryInfo> queryInfoCache;
  
      /** The database we're currently using. */
      private String database = null;
  
      /** Internal DBMD to use for various database-version specific features */
      private DatabaseMetaData dbmd = null;
  
      private NativeSession session = null;
  
      /** Is this connection associated with a global tx? */
      private boolean isInGlobalTx = false;
  
      /** isolation level */
      private int isolationLevel = java.sql.Connection.TRANSACTION_READ_COMMITTED;
  
      /**
       * An array of currently open statements.
       * Copy-on-write used here to avoid ConcurrentModificationException when statements unregister themselves while we iterate over the list.
       */
      private final CopyOnWriteArrayList<JdbcStatement> openStatements = new CopyOnWriteArrayList<>();
  
      private LRUCache<CompoundCacheKey, CallableStatement.CallableStatementParamInfo> parsedCallableStatementCache;
  
      /** The password we used */
      private String password = null;
  
      /** Properties for this connection specified by user */
      protected Properties props = null;
  
      /** Are we in read-only mode? */
      private boolean readOnly = false;
  
      /** Cache of ResultSet metadata */
      protected LRUCache<String, CachedResultSetMetaData> resultSetMetadataCache;
  
      /**
       * The type map for UDTs (not implemented, but used by some third-party
       * vendors, most notably IBM WebSphere)
       */
      private Map<String, Class<?>> typeMap;
  
      /** The user we're connected as */
      private String user = null;
  
      private LRUCache<String, Boolean> serverSideStatementCheckCache;
      private LRUCache<CompoundCacheKey, ServerPreparedStatement> serverSideStatementCache;
  
      private HostInfo origHostInfo;
  
      private String origHostToConnectTo;
  
      // we don't want to be able to publicly clone this...
  
      private int origPortToConnectTo;
  
      /*
       * For testing failover scenarios
       */
      private boolean hasTriedSourceFlag = false;
  
      private List<QueryInterceptor> queryInterceptors;
  
      protected JdbcPropertySet propertySet;
  
      private RuntimeProperty<Boolean> autoReconnectForPools;
      private RuntimeProperty<Boolean> cachePrepStmts;
      private RuntimeProperty<Boolean> autoReconnect;
      private RuntimeProperty<Boolean> useUsageAdvisor;
      private RuntimeProperty<Boolean> reconnectAtTxEnd;
      private RuntimeProperty<Boolean> emulateUnsupportedPstmts;
      private RuntimeProperty<Boolean> ignoreNonTxTables;
      private RuntimeProperty<Boolean> pedantic;
      private RuntimeProperty<Integer> prepStmtCacheSqlLimit;
      private RuntimeProperty<Boolean> useLocalSessionState;
      private RuntimeProperty<Boolean> useServerPrepStmts;
      private RuntimeProperty<Boolean> processEscapeCodesForPrepStmts;
      private RuntimeProperty<Boolean> useLocalTransactionState;
      private RuntimeProperty<Boolean> disconnectOnExpiredPasswords;
      private RuntimeProperty<Boolean> readOnlyPropagatesToServer;
  
      protected ResultSetFactory nullStatementResultSetFactory;
  
      /**
       * '
       * For the delegate only
       */
      protected ConnectionImpl() {
      }
  
      /**
       * Creates a connection to a MySQL Server.
       * 
       * @param hostInfo
       *            the {@link HostInfo} instance that contains the host, user and connections attributes for this connection
       * @exception SQLException
       *                if a database access error occurs
       */
      public ConnectionImpl(HostInfo hostInfo) throws SQLException {
  
          try {
              // Stash away for later, used to clone this connection for Statement.cancel and Statement.setQueryTimeout().
              this.origHostInfo = hostInfo;
              this.origHostToConnectTo = hostInfo.getHost();
              this.origPortToConnectTo = hostInfo.getPort();
  
              this.database = hostInfo.getDatabase();
              this.user = hostInfo.getUser();
              this.password = hostInfo.getPassword();
  
              this.props = hostInfo.exposeAsProperties();
  
              this.propertySet = new JdbcPropertySetImpl();
  
              this.propertySet.initializeProperties(this.props);
  
              // We need Session ASAP to get access to central driver functionality
              this.nullStatementResultSetFactory = new ResultSetFactory(this, null);
              this.session = new NativeSession(hostInfo, this.propertySet);
              this.session.addListener(this); // listen for session status changes
  
              // we can't cache fixed values here because properties are still not initialized with user provided values
              this.autoReconnectForPools = this.propertySet.getBooleanProperty(PropertyKey.autoReconnectForPools);
              this.cachePrepStmts = this.propertySet.getBooleanProperty(PropertyKey.cachePrepStmts);
              this.autoReconnect = this.propertySet.getBooleanProperty(PropertyKey.autoReconnect);
              this.useUsageAdvisor = this.propertySet.getBooleanProperty(PropertyKey.useUsageAdvisor);
              this.reconnectAtTxEnd = this.propertySet.getBooleanProperty(PropertyKey.reconnectAtTxEnd);
              this.emulateUnsupportedPstmts = this.propertySet.getBooleanProperty(PropertyKey.emulateUnsupportedPstmts);
              this.ignoreNonTxTables = this.propertySet.getBooleanProperty(PropertyKey.ignoreNonTxTables);
              this.pedantic = this.propertySet.getBooleanProperty(PropertyKey.pedantic);
              this.prepStmtCacheSqlLimit = this.propertySet.getIntegerProperty(PropertyKey.prepStmtCacheSqlLimit);
              this.useLocalSessionState = this.propertySet.getBooleanProperty(PropertyKey.useLocalSessionState);
              this.useServerPrepStmts = this.propertySet.getBooleanProperty(PropertyKey.useServerPrepStmts);
              this.processEscapeCodesForPrepStmts = this.propertySet.getBooleanProperty(PropertyKey.processEscapeCodesForPrepStmts);
              this.useLocalTransactionState = this.propertySet.getBooleanProperty(PropertyKey.useLocalTransactionState);
              this.disconnectOnExpiredPasswords = this.propertySet.getBooleanProperty(PropertyKey.disconnectOnExpiredPasswords);
              this.readOnlyPropagatesToServer = this.propertySet.getBooleanProperty(PropertyKey.readOnlyPropagatesToServer);
  
              String exceptionInterceptorClasses = this.propertySet.getStringProperty(PropertyKey.exceptionInterceptors).getStringValue();
              if (exceptionInterceptorClasses != null && !"".equals(exceptionInterceptorClasses)) {
                  this.exceptionInterceptor = new ExceptionInterceptorChain(exceptionInterceptorClasses, this.props, this.session.getLog());
              }
  
              if (this.cachePrepStmts.getValue()) {
                  createPreparedStatementCaches();
              }
  
              if (this.propertySet.getBooleanProperty(PropertyKey.cacheCallableStmts).getValue()) {
                  this.parsedCallableStatementCache = new LRUCache<>(this.propertySet.getIntegerProperty(PropertyKey.callableStmtCacheSize).getValue());
              }
  
              if (this.propertySet.getBooleanProperty(PropertyKey.allowMultiQueries).getValue()) {
                  this.propertySet.getProperty(PropertyKey.cacheResultSetMetadata).setValue(false); // we don't handle this yet
              }
  
              if (this.propertySet.getBooleanProperty(PropertyKey.cacheResultSetMetadata).getValue()) {
                  this.resultSetMetadataCache = new LRUCache<>(this.propertySet.getIntegerProperty(PropertyKey.metadataCacheSize).getValue());
              }
  
              if (this.propertySet.getStringProperty(PropertyKey.socksProxyHost).getStringValue() != null) {
                  this.propertySet.getProperty(PropertyKey.socketFactory).setValue(SocksProxySocketFactory.class.getName());
              }
  
              this.dbmd = getMetaData(false, false);
  
              initializeSafeQueryInterceptors();
  
          } catch (CJException e1) {
              throw SQLExceptionsMapping.translateException(e1, getExceptionInterceptor());
          }
  
          try {
              createNewIO(false);
  
              unSafeQueryInterceptors();
  
              AbandonedConnectionCleanupThread.trackConnection(this, this.getSession().getNetworkResources());
          } catch (SQLException ex) {
              cleanup(ex);
  
              // don't clobber SQL exceptions
              throw ex;
          } catch (Exception ex) {
              cleanup(ex);
  
              throw SQLError
                      .createSQLException(
                              this.propertySet.getBooleanProperty(PropertyKey.paranoid).getValue() ? Messages.getString("Connection.0")
                                      : Messages.getString("Connection.1",
                                              new Object[] { this.session.getHostInfo().getHost(), this.session.getHostInfo().getPort() }),
                              MysqlErrorNumbers.SQL_STATE_COMMUNICATION_LINK_FAILURE, ex, getExceptionInterceptor());
          }
  
      }
  
      @Override
      public JdbcPropertySet getPropertySet() {
          return this.propertySet;
      }
  
      @Override
      public void unSafeQueryInterceptors() throws SQLException {
          this.queryInterceptors = this.queryInterceptors.stream().map(u -> ((NoSubInterceptorWrapper) u).getUnderlyingInterceptor())
                  .collect(Collectors.toList());
  
          if (this.session != null) {
              this.session.setQueryInterceptors(this.queryInterceptors);
          }
      }
  
      @Override
      public void initializeSafeQueryInterceptors() throws SQLException {
          this.queryInterceptors = Util
                  .<QueryInterceptor>loadClasses(this.propertySet.getStringProperty(PropertyKey.queryInterceptors).getStringValue(),
                          "MysqlIo.BadQueryInterceptor", getExceptionInterceptor())
                  .stream().map(o -> new NoSubInterceptorWrapper(o.init(this, this.props, this.session.getLog()))).collect(Collectors.toList());
      }
  
      @Override
      public List<QueryInterceptor> getQueryInterceptorsInstances() {
          return this.queryInterceptors;
      }
  
      private boolean canHandleAsServerPreparedStatement(String sql) throws SQLException {
          if (sql == null || sql.length() == 0) {
              return true;
          }
  
          if (!this.useServerPrepStmts.getValue()) {
              return false;
          }
  
          boolean allowMultiQueries = this.propertySet.getBooleanProperty(PropertyKey.allowMultiQueries).getValue();
  
          if (this.cachePrepStmts.getValue()) {
              synchronized (this.serverSideStatementCheckCache) {
                  Boolean flag = this.serverSideStatementCheckCache.get(sql);
  
                  if (flag != null) {
                      return flag.booleanValue();
                  }
  
                  boolean canHandle = StringUtils.canHandleAsServerPreparedStatementNoCache(sql, getServerVersion(), allowMultiQueries,
                          this.session.getServerSession().isNoBackslashEscapesSet(), this.session.getServerSession().useAnsiQuotedIdentifiers());
  
                  if (sql.length() < this.prepStmtCacheSqlLimit.getValue()) {
                      this.serverSideStatementCheckCache.put(sql, canHandle ? Boolean.TRUE : Boolean.FALSE);
                  }
  
                  return canHandle;
              }
          }
  
          return StringUtils.canHandleAsServerPreparedStatementNoCache(sql, getServerVersion(), allowMultiQueries,
                  this.session.getServerSession().isNoBackslashEscapesSet(), this.session.getServerSession().useAnsiQuotedIdentifiers());
      }
  
      @Override
      public void changeUser(String userName, String newPassword) throws SQLException {
          synchronized (getConnectionMutex()) {
              checkClosed();
  
              if ((userName == null) || userName.equals("")) {
                  userName = "";
              }
  
              if (newPassword == null) {
                  newPassword = "";
              }
  
              try {
                  this.session.changeUser(userName, newPassword, this.database);
              } catch (CJException ex) {
                  // After Bug#16241992 fix the server doesn't return to previous credentials if COM_CHANGE_USER attempt failed. 
                  if ("28000".equals(ex.getSQLState())) {
                      cleanup(ex);
                  }
                  throw ex;
              }
              this.user = userName;
              this.password = newPassword;
  
              this.session.getServerSession().getCharsetSettings().configurePostHandshake(true);
  
              this.session.setSessionVariables();
  
              setupServerForTruncationChecks();
          }
      }
  
      @Override
      public void checkClosed() {
          this.session.checkClosed();
      }
  
      @Override
      public void throwConnectionClosedException() throws SQLException {
          SQLException ex = SQLError.createSQLException(Messages.getString("Connection.2"), MysqlErrorNumbers.SQL_STATE_CONNECTION_NOT_OPEN,
                  getExceptionInterceptor());
  
          if (this.session.getForceClosedReason() != null) {
              ex.initCause(this.session.getForceClosedReason());
          }
  
          throw ex;
      }
  
      /**
       * Set transaction isolation level to the value received from server if any.
       * Is called by connectionInit(...)
       */
      private void checkTransactionIsolationLevel() {
          String s = this.session.getServerSession().getServerVariable("transaction_isolation");
          if (s == null) {
              s = this.session.getServerSession().getServerVariable("tx_isolation");
          }
  
          if (s != null) {
              Integer intTI = mapTransIsolationNameToValue.get(s);
  
              if (intTI != null) {
                  this.isolationLevel = intTI.intValue();
              }
          }
      }
  
      @Override
      public void abortInternal() throws SQLException {
          this.session.forceClose();
      }
  
      @Override
      public void cleanup(Throwable whyCleanedUp) {
          try {
              if (this.session != null) {
                  if (isClosed()) {
                      this.session.forceClose();
                  } else {
                      realClose(false, false, false, whyCleanedUp);
                  }
              }
          } catch (SQLException | CJException sqlEx) {
              // ignore, we're going away.
          }
      }
  
      @Deprecated
      @Override
      public void clearHasTriedMaster() {
          this.hasTriedSourceFlag = false;
      }
  
      @Override
      public void clearWarnings() throws SQLException {
          // firstWarning = null;
      }
  
      @Override
      public java.sql.PreparedStatement clientPrepareStatement(String sql) throws SQLException {
          return clientPrepareStatement(sql, DEFAULT_RESULT_SET_TYPE, DEFAULT_RESULT_SET_CONCURRENCY);
      }
  
      @Override
      public java.sql.PreparedStatement clientPrepareStatement(String sql, int autoGenKeyIndex) throws SQLException {
          java.sql.PreparedStatement pStmt = clientPrepareStatement(sql);
  
          ((ClientPreparedStatement) pStmt).setRetrieveGeneratedKeys(autoGenKeyIndex == java.sql.Statement.RETURN_GENERATED_KEYS);
  
          return pStmt;
      }
  
      @Override
      public java.sql.PreparedStatement clientPrepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
          return clientPrepareStatement(sql, resultSetType, resultSetConcurrency, true);
      }
  
      public java.sql.PreparedStatement clientPrepareStatement(String sql, int resultSetType, int resultSetConcurrency, boolean processEscapeCodesIfNeeded)
              throws SQLException {
  
          String nativeSql = processEscapeCodesIfNeeded && this.processEscapeCodesForPrepStmts.getValue() ? nativeSQL(sql) : sql;
  
          ClientPreparedStatement pStmt = null;
  
          if (this.cachePrepStmts.getValue()) {
              QueryInfo pStmtInfo = this.queryInfoCache.get(nativeSql);
  
              if (pStmtInfo == null) {
                  pStmt = ClientPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database);
  
                  this.queryInfoCache.put(nativeSql, pStmt.getQueryInfo());
              } else {
                  pStmt = ClientPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database, pStmtInfo);
              }
          } else {
              pStmt = ClientPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database);
          }
  
          pStmt.setResultSetType(resultSetType);
          pStmt.setResultSetConcurrency(resultSetConcurrency);
  
          return pStmt;
      }
  
      @Override
      public java.sql.PreparedStatement clientPrepareStatement(String sql, int[] autoGenKeyIndexes) throws SQLException {
  
          ClientPreparedStatement pStmt = (ClientPreparedStatement) clientPrepareStatement(sql);
  
          pStmt.setRetrieveGeneratedKeys((autoGenKeyIndexes != null) && (autoGenKeyIndexes.length > 0));
  
          return pStmt;
      }
  
      @Override
      public java.sql.PreparedStatement clientPrepareStatement(String sql, String[] autoGenKeyColNames) throws SQLException {
          ClientPreparedStatement pStmt = (ClientPreparedStatement) clientPrepareStatement(sql);
  
          pStmt.setRetrieveGeneratedKeys((autoGenKeyColNames != null) && (autoGenKeyColNames.length > 0));
  
          return pStmt;
      }
  
      @Override
      public java.sql.PreparedStatement clientPrepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability)
              throws SQLException {
          return clientPrepareStatement(sql, resultSetType, resultSetConcurrency, true);
      }
  
      @Override
      public void close() throws SQLException {
          synchronized (getConnectionMutex()) {
              if (this.connectionLifecycleInterceptors != null) {
                  for (ConnectionLifecycleInterceptor cli : this.connectionLifecycleInterceptors) {
                      cli.close();
                  }
              }
  
              realClose(true, true, false, null);
          }
      }
  
      @Override
      public void normalClose() {
          try {
              close();
          } catch (SQLException e) {
              ExceptionFactory.createException(e.getMessage(), e);
          }
      }
  
      /**
       * Closes all currently open statements.
       * 
       * @throws SQLException
       *             if a database access error occurs
       */
      private void closeAllOpenStatements() throws SQLException {
          SQLException postponedException = null;
  
          for (JdbcStatement stmt : this.openStatements) {
              try {
                  ((StatementImpl) stmt).realClose(false, true);
              } catch (SQLException sqlEx) {
                  postponedException = sqlEx; // throw it later, cleanup all statements first
              }
          }
  
          if (postponedException != null) {
              throw postponedException;
          }
      }
  
      private void closeStatement(java.sql.Statement stmt) {
          if (stmt != null) {
              try {
                  stmt.close();
              } catch (SQLException sqlEx) {
                  // ignore
              }
  
              stmt = null;
          }
      }
  
      @Override
      public void commit() throws SQLException {
          synchronized (getConnectionMutex()) {
              checkClosed();
  
              try {
                  if (this.connectionLifecycleInterceptors != null) {
                      IterateBlock<ConnectionLifecycleInterceptor> iter = new IterateBlock<ConnectionLifecycleInterceptor>(
                              this.connectionLifecycleInterceptors.iterator()) {
  
                          @Override
                          void forEach(ConnectionLifecycleInterceptor each) throws SQLException {
                              if (!each.commit()) {
                                  this.stopIterating = true;
                              }
                          }
                      };
  
                      iter.doForAll();
  
                      if (!iter.fullIteration()) {
                          return;
                      }
                  }
  
                  if (this.session.getServerSession().isAutoCommit()) {
                      throw SQLError.createSQLException(Messages.getString("Connection.3"), getExceptionInterceptor());
                  }
                  if (this.useLocalTransactionState.getValue()) {
                      if (!this.session.getServerSession().inTransactionOnServer()) {
                          return; // effectively a no-op
                      }
                  }
  
                  this.session.execSQL(null, "commit", -1, null, false, this.nullStatementResultSetFactory, null, false);
              } catch (SQLException sqlException) {
                  if (MysqlErrorNumbers.SQL_STATE_COMMUNICATION_LINK_FAILURE.equals(sqlException.getSQLState())) {
                      throw SQLError.createSQLException(Messages.getString("Connection.4"), MysqlErrorNumbers.SQL_STATE_TRANSACTION_RESOLUTION_UNKNOWN,
                              getExceptionInterceptor());
                  }
  
                  throw sqlException;
              } finally {
                  this.session.setNeedsPing(this.reconnectAtTxEnd.getValue());
              }
          }
          return;
      }
  
      @Override
      public void createNewIO(boolean isForReconnect) {
          synchronized (getConnectionMutex()) {
              // Synchronization Not needed for *new* connections, but definitely for connections going through fail-over, since we might get the new connection
              // up and running *enough* to start sending cached or still-open server-side prepared statements over to the backend before we get a chance to
              // re-prepare them...
  
              try {
                  if (!this.autoReconnect.getValue()) {
                      connectOneTryOnly(isForReconnect);
  
                      return;
                  }
  
                  connectWithRetries(isForReconnect);
              } catch (SQLException ex) {
                  throw ExceptionFactory.createException(UnableToConnectException.class, ex.getMessage(), ex);
              }
          }
      }
  
      private void connectWithRetries(boolean isForReconnect) throws SQLException {
          double timeout = this.propertySet.getIntegerProperty(PropertyKey.initialTimeout).getValue();
          boolean connectionGood = false;
  
          Exception connectionException = null;
  
          for (int attemptCount = 0; (attemptCount < this.propertySet.getIntegerProperty(PropertyKey.maxReconnects).getValue())
                  && !connectionGood; attemptCount++) {
              try {
                  this.session.forceClose();
  
                  JdbcConnection c = getProxy();
                  this.session.connect(this.origHostInfo, this.user, this.password, this.database, getLoginTimeout(), c);
                  pingInternal(false, 0);
  
                  boolean oldAutoCommit;
                  int oldIsolationLevel;
                  boolean oldReadOnly;
                  String oldDb;
  
                  synchronized (getConnectionMutex()) {
                      // save state from old connection
                      oldAutoCommit = getAutoCommit();
                      oldIsolationLevel = this.isolationLevel;
                      oldReadOnly = isReadOnly(false);
                      oldDb = getDatabase();
  
                      this.session.setQueryInterceptors(this.queryInterceptors);
                  }
  
                  // Server properties might be different from previous connection, so initialize again...
                  initializePropsFromServer();
  
                  if (isForReconnect) {
                      // Restore state from old connection
                      setAutoCommit(oldAutoCommit);
                      setTransactionIsolation(oldIsolationLevel);
                      setDatabase(oldDb);
                      setReadOnly(oldReadOnly);
                  }
  
                  connectionGood = true;
  
                  break;
              } catch (UnableToConnectException rejEx) {
                  close();
                  this.session.getProtocol().getSocketConnection().forceClose();
  
              } catch (Exception EEE) {
                  connectionException = EEE;
                  connectionGood = false;
              }
  
              if (connectionGood) {
                  break;
              }
  
              if (attemptCount > 0) {
                  try {
                      Thread.sleep((long) timeout * 1000);
                  } catch (InterruptedException IE) {
                      // ignore
                  }
              }
          } // end attempts for a single host
  
          if (!connectionGood) {
              // We've really failed!
              SQLException chainedEx = SQLError.createSQLException(
                      Messages.getString("Connection.UnableToConnectWithRetries",
                              new Object[] { this.propertySet.getIntegerProperty(PropertyKey.maxReconnects).getValue() }),
                      MysqlErrorNumbers.SQL_STATE_UNABLE_TO_CONNECT_TO_DATASOURCE, connectionException, getExceptionInterceptor());
              throw chainedEx;
          }
  
          if (this.propertySet.getBooleanProperty(PropertyKey.paranoid).getValue() && !this.autoReconnect.getValue()) {
              this.password = null;
              this.user = null;
          }
  
          if (isForReconnect) {
              //
              // Retrieve any 'lost' prepared statements if re-connecting
              //
              Iterator<JdbcStatement> statementIter = this.openStatements.iterator();
  
              //
              // We build a list of these outside the map of open statements, because in the process of re-preparing, we might end up having to close a prepared
              // statement, thus removing it from the map, and generating a ConcurrentModificationException
              //
              Stack<JdbcStatement> serverPreparedStatements = null;
  
              while (statementIter.hasNext()) {
                  JdbcStatement statementObj = statementIter.next();
  
                  if (statementObj instanceof ServerPreparedStatement) {
                      if (serverPreparedStatements == null) {
                          serverPreparedStatements = new Stack<>();
                      }
  
                      serverPreparedStatements.add(statementObj);
                  }
              }
  
              if (serverPreparedStatements != null) {
                  while (!serverPreparedStatements.isEmpty()) {
                      ((ServerPreparedStatement) serverPreparedStatements.pop()).rePrepare();
                  }
              }
          }
      }
  
      private void connectOneTryOnly(boolean isForReconnect) throws SQLException {
          Exception connectionNotEstablishedBecause = null;
  
          try {
  
              JdbcConnection c = getProxy();
              this.session.connect(this.origHostInfo, this.user, this.password, this.database, getLoginTimeout(), c);
  
              // save state from old connection
              boolean oldAutoCommit = getAutoCommit();
              int oldIsolationLevel = this.isolationLevel;
              boolean oldReadOnly = isReadOnly(false);
              String oldDb = getDatabase();
  
              this.session.setQueryInterceptors(this.queryInterceptors);
  
              // Server properties might be different from previous connection, so initialize again...
              initializePropsFromServer();
  
              if (isForReconnect) {
                  // Restore state from old connection
                  setAutoCommit(oldAutoCommit);
                  setTransactionIsolation(oldIsolationLevel);
                  setDatabase(oldDb);
                  setReadOnly(oldReadOnly);
              }
              return;
  
          } catch (UnableToConnectException rejEx) {
              close();
              this.session.getProtocol().getSocketConnection().forceClose();
              throw rejEx;
  
          } catch (Exception EEE) {
  
              if ((EEE instanceof PasswordExpiredException
                      || EEE instanceof SQLException && ((SQLException) EEE).getErrorCode() == MysqlErrorNumbers.ER_MUST_CHANGE_PASSWORD)
                      && !this.disconnectOnExpiredPasswords.getValue()) {
                  return;
              }
  
              if (this.session != null) {
                  this.session.forceClose();
              }
  
              connectionNotEstablishedBecause = EEE;
  
              if (EEE instanceof SQLException) {
                  throw (SQLException) EEE;
              }
  
              if (EEE.getCause() != null && EEE.getCause() instanceof SQLException) {
                  throw (SQLException) EEE.getCause();
              }
  
              if (EEE instanceof CJException) {
                  throw (CJException) EEE;
              }
  
              SQLException chainedEx = SQLError.createSQLException(Messages.getString("Connection.UnableToConnect"),
                      MysqlErrorNumbers.SQL_STATE_UNABLE_TO_CONNECT_TO_DATASOURCE, getExceptionInterceptor());
              chainedEx.initCause(connectionNotEstablishedBecause);
  
              throw chainedEx;
          }
      }
  
      private int getLoginTimeout() {
          int loginTimeoutSecs = DriverManager.getLoginTimeout();
          if (loginTimeoutSecs <= 0) {
              return 0;
          }
          return loginTimeoutSecs * 1000;
      }
  
      private void createPreparedStatementCaches() throws SQLException {
          synchronized (getConnectionMutex()) {
              int cacheSize = this.propertySet.getIntegerProperty(PropertyKey.prepStmtCacheSize).getValue();
              String queryInfoCacheFactory = this.propertySet.getStringProperty(PropertyKey.queryInfoCacheFactory).getValue();
  
              try {
                  Class<?> factoryClass;
  
                  factoryClass = Class.forName(queryInfoCacheFactory);
  
                  @SuppressWarnings("unchecked")
                  CacheAdapterFactory<String, QueryInfo> cacheFactory = ((CacheAdapterFactory<String, QueryInfo>) factoryClass.newInstance());
  
                  this.queryInfoCache = cacheFactory.getInstance(this, this.origHostInfo.getDatabaseUrl(), cacheSize, this.prepStmtCacheSqlLimit.getValue());
  
              } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
                  SQLException sqlEx = SQLError.createSQLException(
                          Messages.getString("Connection.CantFindCacheFactory", new Object[] { queryInfoCacheFactory, PropertyKey.queryInfoCacheFactory }),
                          getExceptionInterceptor());
                  sqlEx.initCause(e);
  
                  throw sqlEx;
              } catch (Exception e) {
                  SQLException sqlEx = SQLError.createSQLException(
                          Messages.getString("Connection.CantLoadCacheFactory", new Object[] { queryInfoCacheFactory, PropertyKey.queryInfoCacheFactory }),
                          getExceptionInterceptor());
                  sqlEx.initCause(e);
  
                  throw sqlEx;
              }
  
              if (this.useServerPrepStmts.getValue()) {
                  this.serverSideStatementCheckCache = new LRUCache<>(cacheSize);
  
                  this.serverSideStatementCache = new LRUCache<CompoundCacheKey, ServerPreparedStatement>(cacheSize) {
  
                      private static final long serialVersionUID = 7692318650375988114L;
  
                      @Override
                      protected boolean removeEldestEntry(java.util.Map.Entry<CompoundCacheKey, ServerPreparedStatement> eldest) {
                          if (this.maxElements <= 1) {
                              return false;
                          }
  
                          boolean removeIt = super.removeEldestEntry(eldest);
  
                          if (removeIt) {
                              ServerPreparedStatement ps = eldest.getValue();
                              ps.isCached = false;
                              ps.setClosed(false);
                              try {
                                  ps.realClose(true, true);
                              } catch (SQLException sqlEx) {
                                  // punt
                              }
                          }
  
                          return removeIt;
                      }
                  };
              }
          }
      }
  
      @Override
      public java.sql.Statement createStatement() throws SQLException {
          return createStatement(DEFAULT_RESULT_SET_TYPE, DEFAULT_RESULT_SET_CONCURRENCY);
      }
  
      @Override
      public java.sql.Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
  
          StatementImpl stmt = new StatementImpl(getMultiHostSafeProxy(), this.database);
          stmt.setResultSetType(resultSetType);
          stmt.setResultSetConcurrency(resultSetConcurrency);
  
          return stmt;
      }
  
      @Override
      public java.sql.Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
          if (this.pedantic.getValue()) {
              if (resultSetHoldability != java.sql.ResultSet.HOLD_CURSORS_OVER_COMMIT) {
                  throw SQLError.createSQLException("HOLD_CUSRORS_OVER_COMMIT is only supported holdability level", MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT,
                          getExceptionInterceptor());
              }
          }
  
          return createStatement(resultSetType, resultSetConcurrency);
      }
  
      @Override
      public int getActiveStatementCount() {
          return this.openStatements.size();
      }
  
      @Override
      public boolean getAutoCommit() throws SQLException {
          synchronized (getConnectionMutex()) {
              return this.session.getServerSession().isAutoCommit();
          }
      }
  
      @Override
      public String getCatalog() throws SQLException {
          synchronized (getConnectionMutex()) {
              return this.propertySet.<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm).getValue() == DatabaseTerm.SCHEMA ? null : this.database;
          }
      }
  
      @Override
      public String getCharacterSetMetadata() {
          synchronized (getConnectionMutex()) {
              return this.session.getServerSession().getCharsetSettings().getMetadataEncoding();
          }
      }
  
      @Override
      public int getHoldability() throws SQLException {
          return java.sql.ResultSet.CLOSE_CURSORS_AT_COMMIT;
      }
  
      @Override
      public long getId() {
          return this.session.getThreadId();
      }
  
      /**
       * NOT JDBC-Compliant, but clients can use this method to determine how long
       * this connection has been idle. This time (reported in milliseconds) is
       * updated once a query has completed.
       * 
       * @return number of ms that this connection has been idle, 0 if the driver
       *         is busy retrieving results.
       */
      @Override
      public long getIdleFor() {
          synchronized (getConnectionMutex()) {
              return this.session.getIdleFor();
          }
      }
  
      @Override
      public java.sql.DatabaseMetaData getMetaData() throws SQLException {
          return getMetaData(true, true);
      }
  
      private java.sql.DatabaseMetaData getMetaData(boolean checkClosed, boolean checkForInfoSchema) throws SQLException {
          if (checkClosed) {
              checkClosed();
          }
  
          com.mysql.cj.jdbc.DatabaseMetaData dbmeta = com.mysql.cj.jdbc.DatabaseMetaData.getInstance(getMultiHostSafeProxy(), this.database, checkForInfoSchema,
                  this.nullStatementResultSetFactory);
  
          if (getSession() != null && getSession().getProtocol() != null) {
              dbmeta.setMetadataEncoding(getSession().getServerSession().getCharsetSettings().getMetadataEncoding());
              dbmeta.setMetadataCollationIndex(getSession().getServerSession().getCharsetSettings().getMetadataCollationIndex());
          }
  
          return dbmeta;
      }
  
      @Override
      public java.sql.Statement getMetadataSafeStatement() throws SQLException {
          return getMetadataSafeStatement(0);
      }
  
      public java.sql.Statement getMetadataSafeStatement(int maxRows) throws SQLException {
          java.sql.Statement stmt = createStatement();
  
          stmt.setMaxRows(maxRows == -1 ? 0 : maxRows);
  
          stmt.setEscapeProcessing(false);
  
          if (stmt.getFetchSize() != 0) {
              stmt.setFetchSize(0);
          }
  
          return stmt;
      }
  
      @Override
      public ServerVersion getServerVersion() {
          return this.session.getServerSession().getServerVersion();
      }
  
      @Override
      public int getTransactionIsolation() throws SQLException {
  
          synchronized (getConnectionMutex()) {
              if (!this.useLocalSessionState.getValue()) {
                  String s = this.session.queryServerVariable(
                          versionMeetsMinimum(8, 0, 3) || (versionMeetsMinimum(5, 7, 20) && !versionMeetsMinimum(8, 0, 0)) ? "@@session.transaction_isolation"
                                  : "@@session.tx_isolation");
  
                  if (s != null) {
                      Integer intTI = mapTransIsolationNameToValue.get(s);
                      if (intTI != null) {
                          this.isolationLevel = intTI.intValue();
                          return this.isolationLevel;
                      }
                      throw SQLError.createSQLException(Messages.getString("Connection.12", new Object[] { s }), MysqlErrorNumbers.SQL_STATE_GENERAL_ERROR,
                              getExceptionInterceptor());
                  }
                  throw SQLError.createSQLException(Messages.getString("Connection.13"), MysqlErrorNumbers.SQL_STATE_GENERAL_ERROR, getExceptionInterceptor());
              }
  
              return this.isolationLevel;
          }
      }
  
      @Override
      public java.util.Map<String, Class<?>> getTypeMap() throws SQLException {
          synchronized (getConnectionMutex()) {
              if (this.typeMap == null) {
                  this.typeMap = new HashMap<>();
              }
  
              return this.typeMap;
          }
      }
  
      @Override
      public String getURL() {
          return this.origHostInfo.getDatabaseUrl();
      }
  
      @Override
      public String getUser() {
          return this.user;
      }
  
      @Override
      public SQLWarning getWarnings() throws SQLException {
          return null;
      }
  
      @Override
      public boolean hasSameProperties(JdbcConnection c) {
          return this.props.equals(c.getProperties());
      }
  
      @Override
      public Properties getProperties() {
          return this.props;
      }
  
      @Deprecated
      @Override
      public boolean hasTriedMaster() {
          return this.hasTriedSourceFlag;
      }
  
      /**
       * Sets varying properties that depend on server information. Called once we
       * have connected to the server.
       * 
       * @throws SQLException
       *             if a database access error occurs
       */
      private void initializePropsFromServer() throws SQLException {
          String connectionInterceptorClasses = this.propertySet.getStringProperty(PropertyKey.connectionLifecycleInterceptors).getStringValue();
  
          this.connectionLifecycleInterceptors = null;
  
          if (connectionInterceptorClasses != null) {
              try {
                  this.connectionLifecycleInterceptors = Util
                          .<ConnectionLifecycleInterceptor>loadClasses(
                                  this.propertySet.getStringProperty(PropertyKey.connectionLifecycleInterceptors).getStringValue(),
                                  "Connection.badLifecycleInterceptor", getExceptionInterceptor())
                          .stream().map(o -> o.init(this, this.props, this.session.getLog())).collect(Collectors.toList());
              } catch (CJException e) {
                  throw SQLExceptionsMapping.translateException(e, getExceptionInterceptor());
              }
          }
  
          this.session.setSessionVariables();
  
          this.session.loadServerVariables(this.getConnectionMutex(), this.dbmd.getDriverVersion());
  
          this.autoIncrementIncrement = this.session.getServerSession().getServerVariable("auto_increment_increment", 1);
  
          try {
              LicenseConfiguration.checkLicenseType(this.session.getServerSession().getServerVariables());
          } catch (CJException e) {
              throw SQLError.createSQLException(e.getMessage(), MysqlErrorNumbers.SQL_STATE_UNABLE_TO_CONNECT_TO_DATASOURCE, getExceptionInterceptor());
          }
  
          this.session.getProtocol().initServerSession();
  
          checkTransactionIsolationLevel();
  
          handleAutoCommitDefaults();
  
          ((com.mysql.cj.jdbc.DatabaseMetaData) this.dbmd).setMetadataEncoding(this.session.getServerSession().getCharsetSettings().getMetadataEncoding());
          ((com.mysql.cj.jdbc.DatabaseMetaData) this.dbmd)
                  .setMetadataCollationIndex(this.session.getServerSession().getCharsetSettings().getMetadataCollationIndex());
  
          //
          // Server can do this more efficiently for us
          //
  
          setupServerForTruncationChecks();
      }
  
      /**
       * Resets a default auto-commit value of 0 to 1, as required by JDBC specification.
       * Takes into account that the default auto-commit value of 0 may have been changed on the server via init_connect.
       * 
       * @throws SQLException
       *             if a database access error occurs
       */
      private void handleAutoCommitDefaults() throws SQLException {
          boolean resetAutoCommitDefault = false;
  
          // Server Bug#66884 (SERVER_STATUS is always initiated with SERVER_STATUS_AUTOCOMMIT=1) invalidates "elideSetAutoCommits" feature.
          // TODO Turn this feature back on as soon as the server bug is fixed. Consider making it version specific.
          // if (!getPropertySet().getBooleanReadableProperty(PropertyKey.elideSetAutoCommits).getValue()) {
          String initConnectValue = this.session.getServerSession().getServerVariable("init_connect");
          if (initConnectValue != null && initConnectValue.length() > 0) {
              // auto-commit might have changed
  
              String s = this.session.queryServerVariable("@@session.autocommit");
              if (s != null) {
                  this.session.getServerSession().setAutoCommit(Boolean.parseBoolean(s));
                  if (!this.session.getServerSession().isAutoCommit()) {
                      resetAutoCommitDefault = true;
                  }
              }
  
          } else {
              // reset it anyway, the server may have been initialized with --autocommit=0
              resetAutoCommitDefault = true;
          }
          //} else if (getSession().isSetNeededForAutoCommitMode(true)) {
          //    // we're not in standard autocommit=true mode
          //    this.session.setAutoCommit(false);
          //    resetAutoCommitDefault = true;
          //}
  
          if (resetAutoCommitDefault) {
              try {
                  setAutoCommit(true); // required by JDBC spec
              } catch (SQLException ex) {
                  if (ex.getErrorCode() != MysqlErrorNumbers.ER_MUST_CHANGE_PASSWORD || this.disconnectOnExpiredPasswords.getValue()) {
                      throw ex;
                  }
              }
          }
      }
  
      @Override
      public boolean isClosed() {
          return this.session.isClosed();
      }
  
      @Override
      public boolean isInGlobalTx() {
          return this.isInGlobalTx;
      }
  
      @Override
      public boolean isSourceConnection() {
          return false; // handled higher up
      }
  
      @Override
      public boolean isReadOnly() throws SQLException {
          return isReadOnly(true);
      }
  
      @Override
      public boolean isReadOnly(boolean useSessionStatus) throws SQLException {
          if (useSessionStatus && !this.session.isClosed() && versionMeetsMinimum(5, 6, 5) && !this.useLocalSessionState.getValue()
                  && this.readOnlyPropagatesToServer.getValue()) {
              String s = this.session.queryServerVariable(
                      versionMeetsMinimum(8, 0, 3) || (versionMeetsMinimum(5, 7, 20) && !versionMeetsMinimum(8, 0, 0)) ? "@@session.transaction_read_only"
                              : "@@session.tx_read_only");
              if (s != null) {
                  return Integer.parseInt(s) != 0; // mysql has a habit of tri+ state booleans
              }
          }
  
          return this.readOnly;
      }
  
      @Override
      public boolean isSameResource(JdbcConnection otherConnection) {
          synchronized (getConnectionMutex()) {
              if (otherConnection == null) {
                  return false;
              }
  
              boolean directCompare = true;
  
              String otherHost = ((ConnectionImpl) otherConnection).origHostToConnectTo;
              String otherOrigDatabase = ((ConnectionImpl) otherConnection).origHostInfo.getDatabase();
              String otherCurrentDb = ((ConnectionImpl) otherConnection).database;
  
              if (!nullSafeCompare(otherHost, this.origHostToConnectTo)) {
                  directCompare = false;
              } else if (otherHost != null && otherHost.indexOf(',') == -1 && otherHost.indexOf(':') == -1) {
                  // need to check port numbers
                  directCompare = (((ConnectionImpl) otherConnection).origPortToConnectTo == this.origPortToConnectTo);
              }
  
              if (directCompare) {
                  if (!nullSafeCompare(otherOrigDatabase, this.origHostInfo.getDatabase()) || !nullSafeCompare(otherCurrentDb, this.database)) {
                      directCompare = false;
                  }
              }
  
              if (directCompare) {
                  return true;
              }
  
              // Has the user explicitly set a resourceId?
              String otherResourceId = ((ConnectionImpl) otherConnection).getPropertySet().getStringProperty(PropertyKey.resourceId).getValue();
              String myResourceId = this.propertySet.getStringProperty(PropertyKey.resourceId).getValue();
  
              if (otherResourceId != null || myResourceId != null) {
                  directCompare = nullSafeCompare(otherResourceId, myResourceId);
  
                  if (directCompare) {
                      return true;
                  }
              }
  
              return false;
          }
      }
  
      private int autoIncrementIncrement = 0;
  
      @Override
      public int getAutoIncrementIncrement() {
          return this.autoIncrementIncrement;
      }
  
      @Override
      public boolean lowerCaseTableNames() {
          return this.session.getServerSession().isLowerCaseTableNames();
      }
  
      @Override
      public String nativeSQL(String sql) throws SQLException {
          if (sql == null) {
              return null;
          }
  
          Object escapedSqlResult = EscapeProcessor.escapeSQL(sql, getMultiHostSafeProxy().getSession().getServerSession().getSessionTimeZone(),
                  getMultiHostSafeProxy().getSession().getServerSession().getCapabilities().serverSupportsFracSecs(),
                  getMultiHostSafeProxy().getSession().getServerSession().isServerTruncatesFracSecs(), getExceptionInterceptor());
  
          if (escapedSqlResult instanceof String) {
              return (String) escapedSqlResult;
          }
  
          return ((EscapeProcessorResult) escapedSqlResult).escapedSql;
      }
  
      private CallableStatement parseCallableStatement(String sql) throws SQLException {
          Object escapedSqlResult = EscapeProcessor.escapeSQL(sql, getMultiHostSafeProxy().getSession().getServerSession().getSessionTimeZone(),
                  getMultiHostSafeProxy().getSession().getServerSession().getCapabilities().serverSupportsFracSecs(),
                  getMultiHostSafeProxy().getSession().getServerSession().isServerTruncatesFracSecs(), getExceptionInterceptor());
  
          boolean isFunctionCall = false;
          String parsedSql = null;
  
          if (escapedSqlResult instanceof EscapeProcessorResult) {
              parsedSql = ((EscapeProcessorResult) escapedSqlResult).escapedSql;
              isFunctionCall = ((EscapeProcessorResult) escapedSqlResult).callingStoredFunction;
          } else {
              parsedSql = (String) escapedSqlResult;
              isFunctionCall = false;
          }
  
          return CallableStatement.getInstance(getMultiHostSafeProxy(), parsedSql, this.database, isFunctionCall);
      }
  
      @Override
      public void ping() throws SQLException {
          pingInternal(true, 0);
      }
  
      @Override
      public void pingInternal(boolean checkForClosedConnection, int timeoutMillis) throws SQLException {
          this.session.ping(checkForClosedConnection, timeoutMillis);
      }
  
      @Override
      public java.sql.CallableStatement prepareCall(String sql) throws SQLException {
  
          return prepareCall(sql, DEFAULT_RESULT_SET_TYPE, DEFAULT_RESULT_SET_CONCURRENCY);
      }
  
      @Override
      public java.sql.CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
          CallableStatement cStmt = null;
  
          if (!this.propertySet.getBooleanProperty(PropertyKey.cacheCallableStmts).getValue()) {
  
              cStmt = parseCallableStatement(sql);
          } else {
              synchronized (this.parsedCallableStatementCache) {
                  CompoundCacheKey key = new CompoundCacheKey(getDatabase(), sql);
  
                  CallableStatement.CallableStatementParamInfo cachedParamInfo = this.parsedCallableStatementCache.get(key);
  
                  if (cachedParamInfo != null) {
                      cStmt = CallableStatement.getInstance(getMultiHostSafeProxy(), cachedParamInfo);
                  } else {
                      cStmt = parseCallableStatement(sql);
  
                      synchronized (cStmt) {
                          cachedParamInfo = cStmt.paramInfo;
                      }
  
                      this.parsedCallableStatementCache.put(key, cachedParamInfo);
                  }
              }
          }
  
          cStmt.setResultSetType(resultSetType);
          cStmt.setResultSetConcurrency(resultSetConcurrency);
  
          return cStmt;
      }
  
      @Override
      public java.sql.CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
          if (this.pedantic.getValue()) {
              if (resultSetHoldability != java.sql.ResultSet.HOLD_CURSORS_OVER_COMMIT) {
                  throw SQLError.createSQLException(Messages.getString("Connection.17"), MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
              }
          }
  
          CallableStatement cStmt = (com.mysql.cj.jdbc.CallableStatement) prepareCall(sql, resultSetType, resultSetConcurrency);
  
          return cStmt;
      }
  
      @Override
      public java.sql.PreparedStatement prepareStatement(String sql) throws SQLException {
          return prepareStatement(sql, DEFAULT_RESULT_SET_TYPE, DEFAULT_RESULT_SET_CONCURRENCY);
      }
  
      @Override
      public java.sql.PreparedStatement prepareStatement(String sql, int autoGenKeyIndex) throws SQLException {
          java.sql.PreparedStatement pStmt = prepareStatement(sql);
  
          ((ClientPreparedStatement) pStmt).setRetrieveGeneratedKeys(autoGenKeyIndex == java.sql.Statement.RETURN_GENERATED_KEYS);
  
          return pStmt;
      }
  
      @Override
      public java.sql.PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
          synchronized (getConnectionMutex()) {
              checkClosed();
  
              //
              // FIXME: Create warnings if can't create results of the given type or concurrency
              //
              ClientPreparedStatement pStmt = null;
  
              boolean canServerPrepare = true;
  
              String nativeSql = this.processEscapeCodesForPrepStmts.getValue() ? nativeSQL(sql) : sql;
  
              if (this.useServerPrepStmts.getValue() && this.emulateUnsupportedPstmts.getValue()) {
                  canServerPrepare = canHandleAsServerPreparedStatement(nativeSql);
              }
  
              if (this.useServerPrepStmts.getValue() && canServerPrepare) {
                  if (this.cachePrepStmts.getValue()) {
                      synchronized (this.serverSideStatementCache) {
                          pStmt = this.serverSideStatementCache.remove(new CompoundCacheKey(this.database, sql));
  
                          if (pStmt != null) {
                              ((com.mysql.cj.jdbc.ServerPreparedStatement) pStmt).setClosed(false);
                              pStmt.clearParameters();
                          }
  
                          if (pStmt == null) {
                              try {
                                  pStmt = ServerPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database, resultSetType,
                                          resultSetConcurrency);
                                  if (sql.length() < this.prepStmtCacheSqlLimit.getValue()) {
                                      ((com.mysql.cj.jdbc.ServerPreparedStatement) pStmt).isCacheable = true;
                                  }
  
                                  pStmt.setResultSetType(resultSetType);
                                  pStmt.setResultSetConcurrency(resultSetConcurrency);
                              } catch (SQLException sqlEx) {
                                  // Punt, if necessary
                                  if (this.emulateUnsupportedPstmts.getValue()) {
                                      pStmt = (ClientPreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);
  
                                      if (sql.length() < this.prepStmtCacheSqlLimit.getValue()) {
                                          this.serverSideStatementCheckCache.put(sql, Boolean.FALSE);
                                      }
                                  } else {
                                      throw sqlEx;
                                  }
                              }
                          }
                      }
                  } else {
                      try {
                          pStmt = ServerPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database, resultSetType, resultSetConcurrency);
  
                          pStmt.setResultSetType(resultSetType);
                          pStmt.setResultSetConcurrency(resultSetConcurrency);
                      } catch (SQLException sqlEx) {
                          // Punt, if necessary
                          if (this.emulateUnsupportedPstmts.getValue()) {
                              pStmt = (ClientPreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);
                          } else {
                              throw sqlEx;
                          }
                      }
                  }
              } else {
                  pStmt = (ClientPreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);
              }
  
              return pStmt;
          }
      }
  
      @Override
      public java.sql.PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
          if (this.pedantic.getValue()) {
              if (resultSetHoldability != java.sql.ResultSet.HOLD_CURSORS_OVER_COMMIT) {
                  throw SQLError.createSQLException(Messages.getString("Connection.17"), MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
              }
          }
  
          return prepareStatement(sql, resultSetType, resultSetConcurrency);
      }
  
      @Override
      public java.sql.PreparedStatement prepareStatement(String sql, int[] autoGenKeyIndexes) throws SQLException {
          java.sql.PreparedStatement pStmt = prepareStatement(sql);
  
          ((ClientPreparedStatement) pStmt).setRetrieveGeneratedKeys((autoGenKeyIndexes != null) && (autoGenKeyIndexes.length > 0));
  
          return pStmt;
      }
  
      @Override
      public java.sql.PreparedStatement prepareStatement(String sql, String[] autoGenKeyColNames) throws SQLException {
          java.sql.PreparedStatement pStmt = prepareStatement(sql);
  
          ((ClientPreparedStatement) pStmt).setRetrieveGeneratedKeys((autoGenKeyColNames != null) && (autoGenKeyColNames.length > 0));
  
          return pStmt;
      }
  
      @Override
      public void realClose(boolean calledExplicitly, boolean issueRollback, boolean skipLocalTeardown, Throwable reason) throws SQLException {
          SQLException sqlEx = null;
  
          if (this.isClosed()) {
              return;
          }
  
          this.session.setForceClosedReason(reason);
  
          try {
              if (!skipLocalTeardown) {
                  if (!getAutoCommit() && issueRollback) {
                      try {
                          rollback();
                      } catch (SQLException ex) {
                          sqlEx = ex;
                      }
                  }
  
                  if (this.propertySet.getBooleanProperty(PropertyKey.gatherPerfMetrics).getValue()) {
                      this.session.getProtocol().getMetricsHolder().reportMetrics(this.session.getLog());
                  }
  
                  if (this.useUsageAdvisor.getValue()) {
                      if (!calledExplicitly) {
                          this.session.getProfilerEventHandler().processEvent(ProfilerEvent.TYPE_USAGE, this.session, null, null, 0, new Throwable(),
                                  Messages.getString("Connection.18"));
                      }
  
                      if (System.currentTimeMillis() - this.session.getConnectionCreationTimeMillis() < 500) {
                          this.session.getProfilerEventHandler().processEvent(ProfilerEvent.TYPE_USAGE, this.session, null, null, 0, new Throwable(),
                                  Messages.getString("Connection.19"));
                      }
                  }
  
                  try {
                      closeAllOpenStatements();
                  } catch (SQLException ex) {
                      sqlEx = ex;
                  }
  
                  this.session.quit();
              } else {
                  this.session.forceClose();
              }
  
              if (this.queryInterceptors != null) {
                  for (int i = 0; i < this.queryInterceptors.size(); i++) {
                      this.queryInterceptors.get(i).destroy();
                  }
              }
  
              if (this.exceptionInterceptor != null) {
                  this.exceptionInterceptor.destroy();
              }
  
          } finally {
              this.openStatements.clear();
              this.queryInterceptors = null;
              this.exceptionInterceptor = null;
              this.nullStatementResultSetFactory = null;
          }
  
          if (sqlEx != null) {
              throw sqlEx;
          }
  
      }
  
      @Override
      public void recachePreparedStatement(JdbcPreparedStatement pstmt) throws SQLException {
          synchronized (getConnectionMutex()) {
              if (this.cachePrepStmts.getValue() && pstmt.isPoolable()) {
                  synchronized (this.serverSideStatementCache) {
                      Object oldServerPrepStmt = this.serverSideStatementCache.put(
                              new CompoundCacheKey(pstmt.getCurrentDatabase(), ((PreparedQuery) pstmt.getQuery()).getOriginalSql()),
                              (ServerPreparedStatement) pstmt);
                      if (oldServerPrepStmt != null && oldServerPrepStmt != pstmt) {
                          ((ServerPreparedStatement) oldServerPrepStmt).isCached = false;
                          ((ServerPreparedStatement) oldServerPrepStmt).setClosed(false);
                          ((ServerPreparedStatement) oldServerPrepStmt).realClose(true, true);
                      }
                  }
              }
          }
      }
  
      @Override
      public void decachePreparedStatement(JdbcPreparedStatement pstmt) throws SQLException {
          synchronized (getConnectionMutex()) {
              if (this.cachePrepStmts.getValue()) {
                  synchronized (this.serverSideStatementCache) {
                      this.serverSideStatementCache.remove(new CompoundCacheKey(pstmt.getCurrentDatabase(), ((PreparedQuery) pstmt.getQuery()).getOriginalSql()));
                  }
              }
          }
      }
  
      @Override
      public void registerStatement(JdbcStatement stmt) {
          this.openStatements.addIfAbsent(stmt);
      }
  
      @Override
      public void releaseSavepoint(Savepoint arg0) throws SQLException {
          // this is a no-op
      }
  
      @Override
      public void resetServerState() throws SQLException {
          if (!this.propertySet.getBooleanProperty(PropertyKey.paranoid).getValue() && (this.session != null)) {
              changeUser(this.user, this.password);
          }
      }
  
      @Override
      public void rollback() throws SQLException {
          synchronized (getConnectionMutex()) {
              checkClosed();
  
              try {
                  if (this.connectionLifecycleInterceptors != null) {
                      IterateBlock<ConnectionLifecycleInterceptor> iter = new IterateBlock<ConnectionLifecycleInterceptor>(
                              this.connectionLifecycleInterceptors.iterator()) {
  
                          @Override
                          void forEach(ConnectionLifecycleInterceptor each) throws SQLException {
                              if (!each.rollback()) {
                                  this.stopIterating = true;
                              }
                          }
                      };
  
                      iter.doForAll();
  
                      if (!iter.fullIteration()) {
                          return;
                      }
                  }
                  if (this.session.getServerSession().isAutoCommit()) {
                      throw SQLError.createSQLException(Messages.getString("Connection.20"), MysqlErrorNumbers.SQL_STATE_CONNECTION_NOT_OPEN,
                              getExceptionInterceptor());
                  }
                  try {
                      rollbackNoChecks();
                  } catch (SQLException sqlEx) {
                      // We ignore non-transactional tables if told to do so
                      if (this.ignoreNonTxTables.getInitialValue() && (sqlEx.getErrorCode() == MysqlErrorNumbers.ER_WARNING_NOT_COMPLETE_ROLLBACK)) {
                          return;
                      }
                      throw sqlEx;
  
                  }
              } catch (SQLException sqlException) {
                  if (MysqlErrorNumbers.SQL_STATE_COMMUNICATION_LINK_FAILURE.equals(sqlException.getSQLState())) {
                      throw SQLError.createSQLException(Messages.getString("Connection.21"), MysqlErrorNumbers.SQL_STATE_TRANSACTION_RESOLUTION_UNKNOWN,
                              getExceptionInterceptor());
                  }
  
                  throw sqlException;
              } finally {
                  this.session.setNeedsPing(this.reconnectAtTxEnd.getValue());
              }
          }
      }
  
      @Override
      public void rollback(final Savepoint savepoint) throws SQLException {
  
          synchronized (getConnectionMutex()) {
              checkClosed();
  
              try {
                  if (this.connectionLifecycleInterceptors != null) {
                      IterateBlock<ConnectionLifecycleInterceptor> iter = new IterateBlock<ConnectionLifecycleInterceptor>(
                              this.connectionLifecycleInterceptors.iterator()) {
  
                          @Override
                          void forEach(ConnectionLifecycleInterceptor each) throws SQLException {
                              if (!each.rollback(savepoint)) {
                                  this.stopIterating = true;
                              }
                          }
                      };
  
                      iter.doForAll();
  
                      if (!iter.fullIteration()) {
                          return;
                      }
                  }
  
                  StringBuilder rollbackQuery = new StringBuilder("ROLLBACK TO SAVEPOINT ");
                  rollbackQuery.append('`');
                  rollbackQuery.append(savepoint.getSavepointName());
                  rollbackQuery.append('`');
  
                  java.sql.Statement stmt = null;
  
                  try {
                      stmt = getMetadataSafeStatement();
  
                      stmt.executeUpdate(rollbackQuery.toString());
                  } catch (SQLException sqlEx) {
                      int errno = sqlEx.getErrorCode();
  
                      if (errno == 1181) {
                          String msg = sqlEx.getMessage();
  
                          if (msg != null) {
                              int indexOfError153 = msg.indexOf("153");
  
                              if (indexOfError153 != -1) {
                                  throw SQLError.createSQLException(Messages.getString("Connection.22", new Object[] { savepoint.getSavepointName() }),
                                          MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, errno, getExceptionInterceptor());
                              }
                          }
                      }
  
                      // We ignore non-transactional tables if told to do so
                      if (this.ignoreNonTxTables.getValue() && (sqlEx.getErrorCode() != MysqlErrorNumbers.ER_WARNING_NOT_COMPLETE_ROLLBACK)) {
                          throw sqlEx;
                      }
  
                      if (MysqlErrorNumbers.SQL_STATE_COMMUNICATION_LINK_FAILURE.equals(sqlEx.getSQLState())) {
                          throw SQLError.createSQLException(Messages.getString("Connection.23"), MysqlErrorNumbers.SQL_STATE_TRANSACTION_RESOLUTION_UNKNOWN,
                                  getExceptionInterceptor());
                      }
  
                      throw sqlEx;
                  } finally {
                      closeStatement(stmt);
                  }
              } finally {
                  this.session.setNeedsPing(this.reconnectAtTxEnd.getValue());
              }
          }
      }
  
      private void rollbackNoChecks() throws SQLException {
          synchronized (getConnectionMutex()) {
              if (this.useLocalTransactionState.getValue()) {
                  if (!this.session.getServerSession().inTransactionOnServer()) {
                      return; // effectively a no-op
                  }
              }
  
              this.session.execSQL(null, "rollback", -1, null, false, this.nullStatementResultSetFactory, null, false);
  
          }
      }
  
      @Override
      public java.sql.PreparedStatement serverPrepareStatement(String sql) throws SQLException {
          String nativeSql = this.processEscapeCodesForPrepStmts.getValue() ? nativeSQL(sql) : sql;
  
          return ServerPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.getDatabase(), DEFAULT_RESULT_SET_TYPE,
                  DEFAULT_RESULT_SET_CONCURRENCY);
      }
  
      @Override
      public java.sql.PreparedStatement serverPrepareStatement(String sql, int autoGenKeyIndex) throws SQLException {
          String nativeSql = this.processEscapeCodesForPrepStmts.getValue() ? nativeSQL(sql) : sql;
  
          ClientPreparedStatement pStmt = ServerPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.getDatabase(), DEFAULT_RESULT_SET_TYPE,
                  DEFAULT_RESULT_SET_CONCURRENCY);
  
          pStmt.setRetrieveGeneratedKeys(autoGenKeyIndex == java.sql.Statement.RETURN_GENERATED_KEYS);
  
          return pStmt;
      }
  
      @Override
      public java.sql.PreparedStatement serverPrepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
          String nativeSql = this.processEscapeCodesForPrepStmts.getValue() ? nativeSQL(sql) : sql;
  
          return ServerPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.getDatabase(), resultSetType, resultSetConcurrency);
      }
  
      @Override
      public java.sql.PreparedStatement serverPrepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability)
              throws SQLException {
          if (this.pedantic.getValue()) {
              if (resultSetHoldability != java.sql.ResultSet.HOLD_CURSORS_OVER_COMMIT) {
                  throw SQLError.createSQLException(Messages.getString("Connection.17"), MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
              }
          }
  
          return serverPrepareStatement(sql, resultSetType, resultSetConcurrency);
      }
  
      @Override
      public java.sql.PreparedStatement serverPrepareStatement(String sql, int[] autoGenKeyIndexes) throws SQLException {
  
          ClientPreparedStatement pStmt = (ClientPreparedStatement) serverPrepareStatement(sql);
  
          pStmt.setRetrieveGeneratedKeys((autoGenKeyIndexes != null) && (autoGenKeyIndexes.length > 0));
  
          return pStmt;
      }
  
      @Override
      public java.sql.PreparedStatement serverPrepareStatement(String sql, String[] autoGenKeyColNames) throws SQLException {
          ClientPreparedStatement pStmt = (ClientPreparedStatement) serverPrepareStatement(sql);
  
          pStmt.setRetrieveGeneratedKeys((autoGenKeyColNames != null) && (autoGenKeyColNames.length > 0));
  
          return pStmt;
      }
  
      @Override
      public void setAutoCommit(final boolean autoCommitFlag) throws SQLException {
          synchronized (getConnectionMutex()) {
              checkClosed();
  
              if (this.connectionLifecycleInterceptors != null) {
                  IterateBlock<ConnectionLifecycleInterceptor> iter = new IterateBlock<ConnectionLifecycleInterceptor>(
                          this.connectionLifecycleInterceptors.iterator()) {
  
                      @Override
                      void forEach(ConnectionLifecycleInterceptor each) throws SQLException {
                          if (!each.setAutoCommit(autoCommitFlag)) {
                              this.stopIterating = true;
                          }
                      }
                  };
  
                  iter.doForAll();
  
                  if (!iter.fullIteration()) {
                      return;
                  }
              }
  
              if (this.autoReconnectForPools.getValue()) {
                  this.autoReconnect.setValue(true);
              }
  
              boolean isAutoCommit = this.session.getServerSession().isAutoCommit();
              try {
                  boolean needsSetOnServer = true;
                  if (this.useLocalSessionState.getValue() && isAutoCommit == autoCommitFlag) {
                      needsSetOnServer = false;
                  } else if (!this.autoReconnect.getValue()) {
                      needsSetOnServer = getSession().isSetNeededForAutoCommitMode(autoCommitFlag);
                  }
  
                  // this internal value must be set first as failover depends on it being set to true to fail over (which is done by most app servers and
                  // connection pools at the end of a transaction), and the driver issues an implicit set based on this value when it (re)-connects to a
                  // server so the value holds across connections
                  this.session.getServerSession().setAutoCommit(autoCommitFlag);
  
                  if (needsSetOnServer) {
                      this.session.execSQL(null, autoCommitFlag ? "SET autocommit=1" : "SET autocommit=0", -1, null, false, this.nullStatementResultSetFactory,
                              null, false);
                  }
              } catch (CJCommunicationsException e) {
                  throw e;
              } catch (CJException e) {
                  // Reset to current autocommit value in case of an exception different than a communication exception occurs.
                  this.session.getServerSession().setAutoCommit(isAutoCommit);
                  // Update the stacktrace.
                  throw SQLError.createSQLException(e.getMessage(), e.getSQLState(), e.getVendorCode(), e.isTransient(), e, getExceptionInterceptor());
              } finally {
                  if (this.autoReconnectForPools.getValue()) {
                      this.autoReconnect.setValue(false);
                  }
              }
  
              return;
          }
      }
  
      @Override
      public void setCatalog(final String catalog) throws SQLException {
          if (this.propertySet.<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm).getValue() == DatabaseTerm.CATALOG) {
              setDatabase(catalog);
          }
      }
  
      public void setDatabase(final String db) throws SQLException {
          synchronized (getConnectionMutex()) {
              checkClosed();
  
              if (db == null) {
                  throw SQLError.createSQLException("Database can not be null", MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
              }
  
              if (this.connectionLifecycleInterceptors != null) {
                  IterateBlock<ConnectionLifecycleInterceptor> iter = new IterateBlock<ConnectionLifecycleInterceptor>(
                          this.connectionLifecycleInterceptors.iterator()) {
  
                      @Override
                      void forEach(ConnectionLifecycleInterceptor each) throws SQLException {
                          if (!each.setDatabase(db)) {
                              this.stopIterating = true;
                          }
                      }
                  };
  
                  iter.doForAll();
  
                  if (!iter.fullIteration()) {
                      return;
                  }
              }
  
              if (this.useLocalSessionState.getValue()) {
                  if (this.session.getServerSession().isLowerCaseTableNames()) {
                      if (this.database.equalsIgnoreCase(db)) {
                          return;
                      }
                  } else {
                      if (this.database.equals(db)) {
                          return;
                      }
                  }
              }
  
              String quotedId = this.session.getIdentifierQuoteString();
  
              if ((quotedId == null) || quotedId.equals(" ")) {
                  quotedId = "";
              }
  
              StringBuilder query = new StringBuilder("USE ");
              query.append(StringUtils.quoteIdentifier(db, quotedId, this.pedantic.getValue()));
  
              this.session.execSQL(null, query.toString(), -1, null, false, this.nullStatementResultSetFactory, null, false);
  
              this.database = db;
          }
      }
  
      @Override
      public String getDatabase() throws SQLException {
          synchronized (getConnectionMutex()) {
              return this.database;
          }
      }
  
      @Override
      public void setFailedOver(boolean flag) {
          // handled higher up
      }
  
      @Override
      public void setHoldability(int arg0) throws SQLException {
          // do nothing
      }
  
      @Override
      public void setInGlobalTx(boolean flag) {
          this.isInGlobalTx = flag;
      }
  
      @Override
      public void setReadOnly(boolean readOnlyFlag) throws SQLException {
  
          setReadOnlyInternal(readOnlyFlag);
      }
  
      @Override
      public void setReadOnlyInternal(boolean readOnlyFlag) throws SQLException {
          synchronized (getConnectionMutex()) {
              // note this this is safe even inside a transaction
              if (this.readOnlyPropagatesToServer.getValue() && versionMeetsMinimum(5, 6, 5)) {
                  if (!this.useLocalSessionState.getValue() || (readOnlyFlag != this.readOnly)) {
                      this.session.execSQL(null, "set session transaction " + (readOnlyFlag ? "read only" : "read write"), -1, null, false,
                              this.nullStatementResultSetFactory, null, false);
                  }
              }
  
              this.readOnly = readOnlyFlag;
          }
      }
  
      @Override
      public java.sql.Savepoint setSavepoint() throws SQLException {
          MysqlSavepoint savepoint = new MysqlSavepoint(getExceptionInterceptor());
  
          setSavepoint(savepoint);
  
          return savepoint;
      }
  
      private void setSavepoint(MysqlSavepoint savepoint) throws SQLException {
  
          synchronized (getConnectionMutex()) {
              checkClosed();
  
              StringBuilder savePointQuery = new StringBuilder("SAVEPOINT ");
              savePointQuery.append('`');
              savePointQuery.append(savepoint.getSavepointName());
              savePointQuery.append('`');
  
              java.sql.Statement stmt = null;
  
              try {
                  stmt = getMetadataSafeStatement();
  
                  stmt.executeUpdate(savePointQuery.toString());
              } finally {
                  closeStatement(stmt);
              }
          }
      }
  
      @Override
      public java.sql.Savepoint setSavepoint(String name) throws SQLException {
          synchronized (getConnectionMutex()) {
              MysqlSavepoint savepoint = new MysqlSavepoint(name, getExceptionInterceptor());
  
              setSavepoint(savepoint);
  
              return savepoint;
          }
      }
  
      @Override
      public void setTransactionIsolation(int level) throws SQLException {
          synchronized (getConnectionMutex()) {
              checkClosed();
  
              String sql = null;
  
              boolean shouldSendSet = false;
  
              if (this.propertySet.getBooleanProperty(PropertyKey.alwaysSendSetIsolation).getValue()) {
                  shouldSendSet = true;
              } else {
                  if (level != this.isolationLevel) {
                      shouldSendSet = true;
                  }
              }
  
              if (this.useLocalSessionState.getValue()) {
                  shouldSendSet = this.isolationLevel != level;
              }
  
              if (shouldSendSet) {
                  switch (level) {
                      case java.sql.Connection.TRANSACTION_NONE:
                          throw SQLError.createSQLException(Messages.getString("Connection.24"), getExceptionInterceptor());
  
                      case java.sql.Connection.TRANSACTION_READ_COMMITTED:
                          sql = "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED";
  
                          break;
  
                      case java.sql.Connection.TRANSACTION_READ_UNCOMMITTED:
                          sql = "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED";
  
                          break;
  
                      case java.sql.Connection.TRANSACTION_REPEATABLE_READ:
                          sql = "SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ";
  
                          break;
  
                      case java.sql.Connection.TRANSACTION_SERIALIZABLE:
                          sql = "SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE";
  
                          break;
  
                      default:
                          throw SQLError.createSQLException(Messages.getString("Connection.25", new Object[] { level }),
                                  MysqlErrorNumbers.SQL_STATE_DRIVER_NOT_CAPABLE, getExceptionInterceptor());
                  }
  
                  this.session.execSQL(null, sql, -1, null, false, this.nullStatementResultSetFactory, null, false);
  
                  this.isolationLevel = level;
              }
          }
      }
  
      @Override
      public void setTypeMap(java.util.Map<String, Class<?>> map) throws SQLException {
          synchronized (getConnectionMutex()) {
              this.typeMap = map;
          }
      }
  
      private void setupServerForTruncationChecks() throws SQLException {
          synchronized (getConnectionMutex()) {
              RuntimeProperty<Boolean> jdbcCompliantTruncation = this.propertySet.getProperty(PropertyKey.jdbcCompliantTruncation);
              if (jdbcCompliantTruncation.getValue()) {
                  String currentSqlMode = this.session.getServerSession().getServerVariable("sql_mode");
  
                  boolean strictTransTablesIsSet = StringUtils.indexOfIgnoreCase(currentSqlMode, "STRICT_TRANS_TABLES") != -1;
  
                  if (currentSqlMode == null || currentSqlMode.length() == 0 || !strictTransTablesIsSet) {
                      StringBuilder commandBuf = new StringBuilder("SET sql_mode='");
  
                      if (currentSqlMode != null && currentSqlMode.length() > 0) {
                          commandBuf.append(currentSqlMode);
                          commandBuf.append(",");
                      }
  
                      commandBuf.append("STRICT_TRANS_TABLES'");
  
                      this.session.execSQL(null, commandBuf.toString(), -1, null, false, this.nullStatementResultSetFactory, null, false);
  
                      jdbcCompliantTruncation.setValue(false); // server's handling this for us now
                  } else if (strictTransTablesIsSet) {
                      // We didn't set it, but someone did, so we piggy back on it
                      jdbcCompliantTruncation.setValue(false); // server's handling this for us now
                  }
              }
          }
      }
  
      @Override
      public void shutdownServer() throws SQLException {
          try {
              this.session.shutdownServer();
          } catch (CJException ex) {
              SQLException sqlEx = SQLError.createSQLException(Messages.getString("Connection.UnhandledExceptionDuringShutdown"),
                      MysqlErrorNumbers.SQL_STATE_GENERAL_ERROR, getExceptionInterceptor());
  
              sqlEx.initCause(ex);
  
              throw sqlEx;
          }
      }
  
      @Override
      public void unregisterStatement(JdbcStatement stmt) {
          this.openStatements.remove(stmt);
      }
  
      public boolean versionMeetsMinimum(int major, int minor, int subminor) {
          return this.session.versionMeetsMinimum(major, minor, subminor);
      }
  
      @Override
      public CachedResultSetMetaData getCachedMetaData(String sql) {
          if (this.resultSetMetadataCache != null) {
              synchronized (this.resultSetMetadataCache) {
                  return this.resultSetMetadataCache.get(sql);
              }
          }
  
          return null; // no cache exists
      }
  
      @Override
      public void initializeResultsMetadataFromCache(String sql, CachedResultSetMetaData cachedMetaData, ResultSetInternalMethods resultSet) throws SQLException {
  
          if (cachedMetaData == null) {
  
              // read from results
              cachedMetaData = new CachedResultSetMetaDataImpl();
  
              // assume that users will use named-based lookups
              resultSet.getColumnDefinition().buildIndexMapping();
              resultSet.initializeWithMetadata();
  
              if (resultSet instanceof UpdatableResultSet) {
                  ((UpdatableResultSet) resultSet).checkUpdatability();
              }
  
              resultSet.populateCachedMetaData(cachedMetaData);
  
              this.resultSetMetadataCache.put(sql, cachedMetaData);
          } else {
              resultSet.getColumnDefinition().initializeFrom(cachedMetaData);
              resultSet.initializeWithMetadata();
  
              if (resultSet instanceof UpdatableResultSet) {
                  ((UpdatableResultSet) resultSet).checkUpdatability();
              }
          }
      }
  
      @Override
      public String getStatementComment() {
          return this.session.getProtocol().getQueryComment();
      }
  
      @Override
      public void setStatementComment(String comment) {
          this.session.getProtocol().setQueryComment(comment);
      }
  
      @Override
      public void transactionBegun() {
          synchronized (getConnectionMutex()) {
              if (this.connectionLifecycleInterceptors != null) {
                  this.connectionLifecycleInterceptors.stream().forEach(ConnectionLifecycleInterceptor::transactionBegun);
              }
          }
      }
  
      @Override
      public void transactionCompleted() {
          synchronized (getConnectionMutex()) {
              if (this.connectionLifecycleInterceptors != null) {
                  this.connectionLifecycleInterceptors.stream().forEach(ConnectionLifecycleInterceptor::transactionCompleted);
              }
          }
      }
  
      @Override
      public boolean storesLowerCaseTableName() {
          return this.session.getServerSession().storesLowerCaseTableNames();
      }
  
      private ExceptionInterceptor exceptionInterceptor;
  
      @Override
      public ExceptionInterceptor getExceptionInterceptor() {
          return this.exceptionInterceptor;
      }
  
      @Override
      public boolean isServerLocal() throws SQLException {
          synchronized (getConnectionMutex()) {
              try {
                  return this.session.isServerLocal(this.getSession());
              } catch (CJException ex) {
                  SQLException sqlEx = SQLExceptionsMapping.translateException(ex, getExceptionInterceptor());
                  throw sqlEx;
              }
          }
      }
  
      @Override
      public int getSessionMaxRows() {
          synchronized (getConnectionMutex()) {
              return this.session.getSessionMaxRows();
          }
      }
  
      @Override
      public void setSessionMaxRows(int max) throws SQLException {
          synchronized (getConnectionMutex()) {
              checkClosed();
              if (this.session.getSessionMaxRows() != max) {
                  this.session.setSessionMaxRows(max);
                  this.session.execSQL(null, "SET SQL_SELECT_LIMIT=" + (this.session.getSessionMaxRows() == -1 ? "DEFAULT" : this.session.getSessionMaxRows()),
                          -1, null, false, this.nullStatementResultSetFactory, null, false);
              }
          }
      }
  
      @Override
      public void setSchema(String schema) throws SQLException {
          checkClosed();
          if (this.propertySet.<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm).getValue() == DatabaseTerm.SCHEMA) {
              setDatabase(schema);
          }
      }
  
      @Override
      public String getSchema() throws SQLException {
          synchronized (getConnectionMutex()) {
              checkClosed();
              return this.propertySet.<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm).getValue() == DatabaseTerm.SCHEMA ? this.database : null;
          }
      }
  
      @Override
      public void abort(Executor executor) throws SQLException {
          SecurityManager sec = System.getSecurityManager();
  
          if (sec != null) {
              sec.checkPermission(ABORT_PERM);
          }
  
          if (executor == null) {
              throw SQLError.createSQLException(Messages.getString("Connection.26"), MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
          }
  
          executor.execute(new Runnable() {
  
              @Override
              public void run() {
                  try {
                      abortInternal();
                  } catch (SQLException e) {
                      throw new RuntimeException(e);
                  }
              }
          });
      }
  
      @Override
      public void setNetworkTimeout(Executor executor, final int milliseconds) throws SQLException {
          synchronized (getConnectionMutex()) {
              SecurityManager sec = System.getSecurityManager();
  
              if (sec != null) {
                  sec.checkPermission(SET_NETWORK_TIMEOUT_PERM);
              }
  
              if (executor == null) {
                  throw SQLError.createSQLException(Messages.getString("Connection.26"), MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
              }
  
              checkClosed();
  
              executor.execute(new NetworkTimeoutSetter(this, milliseconds));
          }
      }
  
      private static class NetworkTimeoutSetter implements Runnable {
          private final WeakReference<JdbcConnection> connRef;
          private final int milliseconds;
  
          public NetworkTimeoutSetter(JdbcConnection conn, int milliseconds) {
              this.connRef = new WeakReference<>(conn);
              this.milliseconds = milliseconds;
          }
  
          @Override
          public void run() {
              JdbcConnection conn = this.connRef.get();
              if (conn != null) {
                  synchronized (conn.getConnectionMutex()) {
                      ((NativeSession) conn.getSession()).setSocketTimeout(this.milliseconds);
                  }
              }
          }
      }
  
      @Override
      public int getNetworkTimeout() throws SQLException {
          synchronized (getConnectionMutex()) {
              checkClosed();
              return this.session.getSocketTimeout();
          }
      }
  
      @Override
      public Clob createClob() {
          return new com.mysql.cj.jdbc.Clob(getExceptionInterceptor());
      }
  
      @Override
      public Blob createBlob() {
          return new com.mysql.cj.jdbc.Blob(getExceptionInterceptor());
      }
  
      @Override
      public NClob createNClob() {
          return new com.mysql.cj.jdbc.NClob(getExceptionInterceptor());
      }
  
      @Override
      public SQLXML createSQLXML() throws SQLException {
          return new MysqlSQLXML(getExceptionInterceptor());
      }
  
      @Override
      public boolean isValid(int timeout) throws SQLException {
          synchronized (getConnectionMutex()) {
              if (isClosed()) {
                  return false;
              }
  
              try {
                  try {
                      pingInternal(false, timeout * 1000);
                  } catch (Throwable t) {
                      try {
                          abortInternal();
                      } catch (Throwable ignoreThrown) {
                          // we're dead now anyway
                      }
  
                      return false;
                  }
  
              } catch (Throwable t) {
                  return false;
              }
  
              return true;
          }
      }
  
      private ClientInfoProvider infoProvider;
  
      @Override
      public ClientInfoProvider getClientInfoProviderImpl() throws SQLException {
          synchronized (getConnectionMutex()) {
              if (this.infoProvider == null) {
                  String clientInfoProvider = this.propertySet.getStringProperty(PropertyKey.clientInfoProvider).getStringValue();
                  try {
                      try {
                          this.infoProvider = (ClientInfoProvider) Util.getInstance(clientInfoProvider, new Class<?>[0], new Object[0],
                                  getExceptionInterceptor());
                      } catch (CJException ex1) {
                          try {
                              // try with package name prepended
                              this.infoProvider = (ClientInfoProvider) Util.getInstance("com.mysql.cj.jdbc." + clientInfoProvider, new Class<?>[0], new Object[0],
                                      getExceptionInterceptor());
                          } catch (CJException ex2) {
                              throw SQLExceptionsMapping.translateException(ex1, getExceptionInterceptor());
                          }
                      }
                  } catch (ClassCastException ex) {
                      throw SQLError.createSQLException(Messages.getString("Connection.ClientInfoNotImplemented", new Object[] { clientInfoProvider }),
                              MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
                  }
  
                  this.infoProvider.initialize(this, this.props);
              }
  
              return this.infoProvider;
          }
      }
  
      @Override
      public void setClientInfo(String name, String value) throws SQLClientInfoException {
          try {
              getClientInfoProviderImpl().setClientInfo(this, name, value);
          } catch (SQLClientInfoException ciEx) {
              throw ciEx;
          } catch (SQLException | CJException sqlEx) {
              SQLClientInfoException clientInfoEx = new SQLClientInfoException();
              clientInfoEx.initCause(sqlEx);
  
              throw clientInfoEx;
          }
      }
  
      @Override
      public void setClientInfo(Properties properties) throws SQLClientInfoException {
          try {
              getClientInfoProviderImpl().setClientInfo(this, properties);
          } catch (SQLClientInfoException ciEx) {
              throw ciEx;
          } catch (SQLException | CJException sqlEx) {
              SQLClientInfoException clientInfoEx = new SQLClientInfoException();
              clientInfoEx.initCause(sqlEx);
  
              throw clientInfoEx;
          }
      }
  
      @Override
      public String getClientInfo(String name) throws SQLException {
          return getClientInfoProviderImpl().getClientInfo(this, name);
      }
  
      @Override
      public Properties getClientInfo() throws SQLException {
          return getClientInfoProviderImpl().getClientInfo(this);
      }
  
      @Override
      public java.sql.Array createArrayOf(String typeName, Object[] elements) throws SQLException {
          throw SQLError.createSQLFeatureNotSupportedException();
      }
  
      @Override
      public Struct createStruct(String typeName, Object[] attributes) throws SQLException {
          throw SQLError.createSQLFeatureNotSupportedException();
      }
  
      @Override
      public <T> T unwrap(java.lang.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(), MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT,
                      getExceptionInterceptor());
          }
      }
  
      @Override
      public boolean isWrapperFor(Class<?> iface) throws SQLException {
  
          // This works for classes that aren't actually wrapping
          // anything
          return iface.isInstance(this);
      }
  
      @Override
      public NativeSession getSession() {
          return this.session;
      }
  
      @Override
      public String getHostPortPair() {
          return this.origHostInfo.getHostPortPair();
      }
  
      @Override
      public void handleNormalClose() {
          try {
              close();
          } catch (SQLException e) {
              ExceptionFactory.createException(e.getMessage(), e);
          }
      }
  
      @Override
      public void handleReconnect() {
          createNewIO(true);
      }
  
      @Override
      public void handleCleanup(Throwable whyCleanedUp) {
          cleanup(whyCleanedUp);
      }
  
      @Override
      public ServerSessionStateController getServerSessionStateController() {
          return this.session.getServerSession().getServerSessionStateController();
      }
  
  }
  