Repository: microsoft/mssql-jdbc
Pull request #1669 (2021-10-13 21:34:17)

Idle Connection Resiliency Feature
Idle Connection Resiliency aims to provide built-in reconnection logic that restores most of the session state in a wide subclass of connection break situations, in which connection failure happens before client starting sending the command.


Patch:
diff --git a/pom.xml b/pom.xml
index 1693e75b..b29a04ae 100644
--- a/pom.xml
+++ b/pom.xml
@@ -41,19 +41,21 @@
 
 	<properties>
 		<!-- Allowed values for excluded Groups here - - - - - - - - - - - - - 
-			xJDBC42 - - - - - For tests not compatible with JDBC 42 Specifications - 
-			xGradle - - - - - For tests not compatible with Gradle Script - - - - - - 
-			xSQLv12 - - - - - For tests not compatible with SQL Server 2008 R2 - 2014 
-			xSQLv14 - - - - - For tests not compatible with SQL Server 2016 - 2017 - 
-			xSQLv15 - - - - - For tests not compatible with SQL Server 2019 - - - - - 
-			xAzureSQLDB - - - For tests not compatible with Azure SQL Database - - - 
-			xAzureSQLDW - - - For tests not compatible with Azure Data Warehouse - - 
-			xAzureSQLMI - - - For tests not compatible with Azure SQL Managed Instance 
-			NTLM - - - - - - For tests using NTLM Authentication mode (excluded by default) 
-			reqExternalSetup For tests requiring external setup (excluded by default) 
-			clientCertAuth - For tests requiring client certificate auth setup (excluded 
-			by default) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+			xJDBC42 - - - - - - For tests not compatible with JDBC 42 Specifications 
+			xGradle - - - - - - For tests not compatible with Gradle Script - - - - - 
+			xSQLv11 - - - - - - For tests not compatible with SQL Server 2012
+			xSQLv12 - - - - - - For tests not compatible with SQL Server 2008 R2 - 2014 
+			xSQLv14 - - - - - - For tests not compatible with SQL Server 2016 - 2017 
+			xSQLv15 - - - - - - For tests not compatible with SQL Server 2019 - - - - 
+			xAzureSQLDB - - - - For tests not compatible with Azure SQL Database - - 
+			xAzureSQLDW - - - - For tests not compatible with Azure Data Warehouse - 
+			xAzureSQLMI - - - - For tests not compatible with Azure SQL Managed Instance 
+			NTLM  - - - - - - - For tests using NTLM Authentication mode (excluded by default) 
+			reqExternalSetup  - For tests requiring external setup (excluded by default)
+			clientCertAuth  - - For tests requiring client certificate authentication setup (excluded by default)
+			- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
 			Default testing enabled with SQL Server 2019 (SQLv15) -->
+		<excludedGroups>xSQLv12,xSQLv15,NTLM,MSI,reqExternalSetup,clientCertAuth,fedAuth</excludedGroups>
 
 		<!-- Use -preview for preview release, leave empty for official release. -->
 		<releaseExt>-preview</releaseExt>
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
index f596a352..23033828 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
@@ -23,6 +23,7 @@ import java.net.InetSocketAddress;
 import java.net.Socket;
 import java.net.SocketAddress;
 import java.net.SocketException;
+import java.net.SocketOption;
 import java.net.SocketTimeoutException;
 import java.nio.Buffer;
 import java.nio.ByteBuffer;
@@ -73,6 +74,78 @@ import javax.net.ssl.X509TrustManager;
 
 import com.microsoft.sqlserver.jdbc.dataclassification.SensitivityClassification;
 
+/**
+ * ExtendedSocketOptions provides methods to keep track of keep alive and socket information.
+ *
+ */
+final class ExtendedSocketOptions {
+    private static class ExtSocketOption<T> implements SocketOption<T> {
+        private final String name;
+        private final Class<T> type;
+
+        ExtSocketOption(String name, Class<T> type) {
+            this.name = name;
+            this.type = type;
+        }
+
+        @Override
+        public String name() {
+            return name;
+        }
+
+        @Override
+        public Class<T> type() {
+            return type;
+        }
+
+        @Override
+        public String toString() {
+            return name;
+        }
+    }
+
+    private ExtendedSocketOptions() {}
+
+    /**
+     * Keep-Alive idle time.
+     *
+     * <p>
+     * The value of this socket option is an {@code Integer} that is the number of seconds of idle time before
+     * keep-alive initiates a probe. The socket option is specific to stream-oriented sockets using the TCP/IP protocol.
+     * The exact semantics of this socket option are system dependent.
+     *
+     * <p>
+     * When the {@link java.net.StandardSocketOptions#SO_KEEPALIVE SO_KEEPALIVE} option is enabled, TCP probes a
+     * connection that has been idle for some amount of time. The default value for this idle period is system
+     * dependent, but is typically 2 hours. The {@code TCP_KEEPIDLE} option can be used to affect this value for a given
+     * socket.
+     *
+     * @since 11
+     */
+    public static final SocketOption<Integer> TCP_KEEPIDLE = new ExtSocketOption<Integer>("TCP_KEEPIDLE",
+            Integer.class);
+
+    /**
+     * Keep-Alive retransmission interval time.
+     *
+     * <p>
+     * The value of this socket option is an {@code Integer} that is the number of seconds to wait before retransmitting
+     * a keep-alive probe. The socket option is specific to stream-oriented sockets using the TCP/IP protocol. The exact
+     * semantics of this socket option are system dependent.
+     *
+     * <p>
+     * When the {@link java.net.StandardSocketOptions#SO_KEEPALIVE SO_KEEPALIVE} option is enabled, TCP probes a
+     * connection that has been idle for some amount of time. If the remote system does not respond to a keep-alive
+     * probe, TCP retransmits the probe after some amount of time. The default value for this retransmission interval is
+     * system dependent, but is typically 75 seconds. The {@code TCP_KEEPINTERVAL} option can be used to affect this
+     * value for a given socket.
+     *
+     * @since 11
+     */
+    public static final SocketOption<Integer> TCP_KEEPINTERVAL = new ExtSocketOption<Integer>("TCP_KEEPINTERVAL",
+            Integer.class);
+}
+
 
 final class TDS {
     // TDS protocol versions
@@ -94,6 +167,7 @@ final class TDS {
     static final int TDS_ROW = 0xD1;
     static final int TDS_NBCROW = 0xD2;
     static final int TDS_ENV_CHG = 0xE3;
+    static final int TDS_SESSION_STATE = 0xE4;
     static final int TDS_SSPI = 0xED;
     static final int TDS_DONE = 0xFD;
     static final int TDS_DONEPROC = 0xFE;
@@ -138,6 +212,7 @@ final class TDS {
 
     static final byte TDS_FEATURE_EXT_UTF8SUPPORT = 0x0A;
     static final byte TDS_FEATURE_EXT_AZURESQLDNSCACHING = 0x0B;
+    static final byte TDS_FEATURE_EXT_SESSIONRECOVERY = 0x01;
 
     static final int TDS_TVP = 0xF3;
     static final int TVP_ROW = 0x01;
@@ -187,6 +262,8 @@ final class TDS {
                 return "TDS_NBCROW (0xD2)";
             case TDS_ENV_CHG:
                 return "TDS_ENV_CHG (0xE3)";
+            case TDS_SESSION_STATE:
+                return "TDS_SESSION_STATE (0xE4)";
             case TDS_SSPI:
                 return "TDS_SSPI (0xED)";
             case TDS_DONE:
@@ -203,6 +280,8 @@ final class TDS {
                 return "TDS_FEATURE_EXT_UTF8SUPPORT (0x0A)";
             case TDS_FEATURE_EXT_AZURESQLDNSCACHING:
                 return "TDS_FEATURE_EXT_AZURESQLDNSCACHING (0x0B)";
+            case TDS_FEATURE_EXT_SESSIONRECOVERY:
+                return "TDS_FEATURE_EXT_SESSIONRECOVERY (0x01)";
             default:
                 return "unknown token (0x" + Integer.toHexString(tdsTokenType).toUpperCase() + ")";
         }
@@ -611,14 +690,14 @@ final class TDSChannel implements Serializable {
     ProxySocket proxySocket = null;
 
     // I/O streams for raw TCP/IP communications with SQL Server
-    private InputStream tcpInputStream;
+    private ProxyInputStream tcpInputStream;
     private OutputStream tcpOutputStream;
 
     // I/O streams providing the communications interface to the driver.
     // For SSL-encrypted connections, these are streams obtained from
     // the SSL socket above. They wrap the underlying TCP streams.
     // For unencrypted connections, they are just the TCP streams themselves.
-    private InputStream inputStream;
+    private ProxyInputStream inputStream;
     private OutputStream outputStream;
 
     /** TDS packet payload logger */
@@ -680,12 +759,13 @@ final class TDSChannel implements Serializable {
             // Set socket options
             tcpSocket.setTcpNoDelay(true);
             tcpSocket.setKeepAlive(true);
+            DriverJDBCVersion.setSocketOptions(tcpSocket, this);
 
             // set SO_TIMEOUT
             int socketTimeout = con.getSocketTimeoutMilliseconds();
             tcpSocket.setSoTimeout(socketTimeout);
 
-            inputStream = tcpInputStream = tcpSocket.getInputStream();
+            inputStream = tcpInputStream = new ProxyInputStream(tcpSocket.getInputStream());
             outputStream = tcpOutputStream = tcpSocket.getOutputStream();
         } catch (IOException ex) {
             SQLServerException.ConvertConnectExceptionToSQLServerException(host, port, con, ex);
@@ -696,10 +776,17 @@ final class TDSChannel implements Serializable {
     /**
      * Disables SSL on this TDS channel.
      */
-    void disableSSL() {
+    synchronized void disableSSL() {
         if (logger.isLoggable(Level.FINER))
             logger.finer(toString() + " Disabling SSL...");
 
+        // Guard in case of disableSSL being called before enableSSL
+        if (proxySocket == null) {
+            if (logger.isLoggable(Level.INFO))
+                logger.finer(toString() + " proxySocket is null, exit early");
+            return;
+        }
+
         /*
          * The mission: To close the SSLSocket and release everything that it is holding onto other than the TCP/IP
          * socket and streams. The challenge: Simply closing the SSLSocket tries to do additional, unnecessary shutdown
@@ -963,6 +1050,16 @@ final class TDSChannel implements Serializable {
     private final class ProxyInputStream extends InputStream {
         private InputStream filteredStream;
 
+        /**
+         * Bytes that have been read by a poll(s).
+         */
+        private int[] cachedBytes = new int[10];;
+
+        /**
+         * How many bytes have been cached.
+         */
+        private int cachedLength = 0;
+
         ProxyInputStream(InputStream is) {
             filteredStream = is;
         }
@@ -971,22 +1068,85 @@ final class TDSChannel implements Serializable {
             filteredStream = is;
         }
 
+        /**
+         * Poll the stream to verify connectivity.
+         * 
+         * @return true if the stream is readable.
+         * @throws IOException
+         *         If an I/O exception occurs.
+         */
+        public synchronized boolean poll() {
+            synchronized (this) {
+                int b;
+                try {
+                    b = filteredStream.read();
+                } catch (SocketTimeoutException e) {
+                    // Not a disconnected socket, so we're good to go
+                    return true;
+                } catch (IOException e) {
+                    return false;
+                }
+
+                if (logger.isLoggable(Level.FINEST)) {
+                    logger.finest(toString() + "poll() - read() returned " + b);
+                }
+
+                if (b == -1) // end-of-stream
+                    return false;
+
+                // if we got here, a byte was read and we need to save it
+                // Increase the size of the cache, if needed (should be very rare).
+                if (cachedBytes.length <= cachedLength) {
+                    int[] temp = new int[cachedBytes.length + 10];
+                    for (int i = 0; i < cachedBytes.length; i++) {
+                        temp[i] = cachedBytes[i];
+                    }
+
+                    cachedBytes = temp;
+                }
+
+                cachedBytes[cachedLength] = b;
+                cachedLength++;
+
+                return true;
+            }
+        }
+
+        private int getOneFromCache() {
+            int result = cachedBytes[0];
+            for (int i = 0; i < cachedLength; i++) {
+                cachedBytes[i] = cachedBytes[i + 1];
+            }
+            cachedLength--;
+
+            return result;
+        }
+
         public long skip(long n) throws IOException {
-            long bytesSkipped;
+            synchronized (this) {
+                long bytesSkipped = 0;
 
-            if (logger.isLoggable(Level.FINEST))
-                logger.finest(toString() + " Skipping " + n + " bytes");
+                if (logger.isLoggable(Level.FINEST))
+                    logger.finest(toString() + " Skipping " + n + " bytes");
 
-            bytesSkipped = filteredStream.skip(n);
+                while (cachedLength > 0 && bytesSkipped < n) {
+                    bytesSkipped++;
+                    getOneFromCache();
+                }
 
-            if (logger.isLoggable(Level.FINEST))
-                logger.finest(toString() + " Skipped " + n + " bytes");
+                if (bytesSkipped < n) {
+                    bytesSkipped += filteredStream.skip(n - bytesSkipped);
+                }
+
+                if (logger.isLoggable(Level.FINEST))
+                    logger.finest(toString() + " Skipped " + n + " bytes");
 
-            return bytesSkipped;
+                return bytesSkipped;
+            }
         }
 
         public int available() throws IOException {
-            int bytesAvailable = filteredStream.available();
+            int bytesAvailable = filteredStream.available() + cachedLength;
 
             if (logger.isLoggable(Level.FINEST))
                 logger.finest(toString() + " " + bytesAvailable + " bytes available");
@@ -1009,30 +1169,59 @@ final class TDSChannel implements Serializable {
             return readInternal(b, 0, b.length);
         }
 
-        public int read(byte b[], int offset, int maxBytes) throws IOException {
+        public int read(byte[] b, int offset, int maxBytes) throws IOException {
             return readInternal(b, offset, maxBytes);
         }
 
-        private int readInternal(byte b[], int offset, int maxBytes) throws IOException {
-            int bytesRead;
+        private int readInternal(byte[] b, int offset, int maxBytes) throws IOException {
+            synchronized (this) {
+                int bytesRead;
 
-            if (logger.isLoggable(Level.FINEST))
-                logger.finest(toString() + " Reading " + maxBytes + " bytes");
+                if (logger.isLoggable(Level.FINEST))
+                    logger.finest(toString() + " Reading " + maxBytes + " bytes");
 
-            try {
-                bytesRead = filteredStream.read(b, offset, maxBytes);
-            } catch (IOException e) {
-                if (logger.isLoggable(Level.FINER))
-                    logger.finer(toString() + " " + e.getMessage());
+                // Optimize for nothing cached
+                if (cachedLength == 0) {
+                    try {
+                        bytesRead = filteredStream.read(b, offset, maxBytes);
+                    } catch (IOException e) {
+                        if (logger.isLoggable(Level.FINER))
+                            logger.finer(toString() + " Reading bytes threw exception:" + e.getMessage());
+                        throw e;
+                    }
+                } else {
+                    int offsetBytesToSkipInCache = Math.min(offset, cachedLength);
+                    for (int i = 0; i < offsetBytesToSkipInCache; i++) {
+                        getOneFromCache();
+                    }
 
-                logger.finer(toString() + " Reading bytes threw exception:" + e.getMessage());
-                throw e;
-            }
+                    byte[] bytesFromCache = new byte[Math.min(maxBytes, cachedLength)];
+                    for (int i = 0; i < bytesFromCache.length; i++) {
+                        bytesFromCache[i] = (byte) getOneFromCache();
+                    }
 
-            if (logger.isLoggable(Level.FINEST))
-                logger.finest(toString() + " Read " + bytesRead + " bytes");
+                    try {
+                        byte[] bytesFromStream = new byte[maxBytes - bytesFromCache.length];
+                        int bytesReadFromStream = filteredStream.read(bytesFromStream,
+                                offset - offsetBytesToSkipInCache, maxBytes - bytesFromCache.length);
+                        bytesRead = bytesFromCache.length + bytesReadFromStream;
+                        System.arraycopy(bytesFromCache, 0, b, 0, bytesFromCache.length);
+                        if (bytesReadFromStream >= 0)
+                            System.arraycopy(bytesFromStream, 0, b, bytesFromCache.length, bytesReadFromStream);
+                    } catch (IOException e) {
+                        if (logger.isLoggable(Level.FINER))
+                            logger.finer(toString() + " " + e.getMessage());
 
-            return bytesRead;
+                        logger.finer(toString() + " Reading bytes threw exception:" + e.getMessage());
+                        throw e;
+                    }
+                }
+
+                if (logger.isLoggable(Level.FINEST))
+                    logger.finest(toString() + " Read " + bytesRead + " bytes");
+
+                return bytesRead;
+            }
         }
 
         public boolean markSupported() {
@@ -1045,17 +1234,21 @@ final class TDSChannel implements Serializable {
         }
 
         public void mark(int readLimit) {
-            if (logger.isLoggable(Level.FINEST))
-                logger.finest(toString() + " Marking next " + readLimit + " bytes");
+            synchronized (this) {
+                if (logger.isLoggable(Level.FINEST))
+                    logger.finest(toString() + " Marking next " + readLimit + " bytes");
 
-            filteredStream.mark(readLimit);
+                filteredStream.mark(readLimit);
+            }
         }
 
         public void reset() throws IOException {
-            if (logger.isLoggable(Level.FINEST))
-                logger.finest(toString() + " Resetting to previous mark");
+            synchronized (this) {
+                if (logger.isLoggable(Level.FINEST))
+                    logger.finest(toString() + " Resetting to previous mark");
 
-            filteredStream.reset();
+                filteredStream.reset();
+            }
         }
 
         public void close() throws IOException {
@@ -1303,13 +1496,11 @@ final class TDSChannel implements Serializable {
         }
 
         public void setSoTimeout(int timeout) throws SocketException {
-            if (logger.isLoggable(Level.FINER))
-                logger.finer(toString() + " Ignoring setSoTimeout");
+            tdsChannel.tcpSocket.setSoTimeout(timeout);
         }
 
         public void setTcpNoDelay(boolean on) throws SocketException {
-            if (logger.isLoggable(Level.FINER))
-                logger.finer(toString() + " Ignoring setTcpNoDelay");
+            tdsChannel.tcpSocket.setTcpNoDelay(on);
         }
 
         public void setTrafficClass(int tc) throws SocketException {
@@ -1333,8 +1524,7 @@ final class TDSChannel implements Serializable {
         }
 
         public void setKeepAlive(boolean on) throws SocketException {
-            if (logger.isLoggable(Level.FINER))
-                logger.finer(toString() + " Ignoring setKeepAlive");
+            tdsChannel.tcpSocket.setKeepAlive(on);
         }
 
         public void setOOBInline(boolean on) throws SocketException {
@@ -1836,7 +2026,7 @@ final class TDSChannel implements Serializable {
             if (logger.isLoggable(Level.FINEST))
                 logger.finest(toString() + " Getting SSL InputStream");
 
-            inputStream = sslSocket.getInputStream();
+            inputStream = new ProxyInputStream(sslSocket.getInputStream());
 
             if (logger.isLoggable(Level.FINEST))
                 logger.finest(toString() + " Getting SSL OutputStream");
@@ -2055,9 +2245,62 @@ final class TDSChannel implements Serializable {
         return is;
     }
 
+    /**
+     * Attempts to poll the input stream to see if the network socket is still connected.
+     * 
+     * @return
+     */
+    final Boolean networkSocketStillConnected() {
+        int origSoTimeout;
+        synchronized (inputStream) {
+            synchronized (outputStream) {
+                if (logger.isLoggable(Level.FINEST)) {
+                    logger.finest(toString() + "(networkSocketStillConnected) Checking for socket disconnect.");
+                }
+
+                try {
+                    origSoTimeout = channelSocket.getSoTimeout();
+                } catch (SocketException e) {
+                    if (logger.isLoggable(Level.FINE)) {
+                        logger.fine(toString()
+                                + "(networkSocketStillConnected) channelSocket.getSoTimeout() failed. Unable to poll connection:"
+                                + e.getMessage());
+                    }
+                    return false;
+                }
+
+                try {
+                    channelSocket.setSoTimeout(1);
+                    boolean pollResult = inputStream.poll();
+                    channelSocket.setSoTimeout(origSoTimeout);
+                    if (logger.isLoggable(Level.FINEST)) {
+                        if (pollResult) {
+                            logger.finest(toString() + "(networkSocketStillConnected) Network still connected.");
+                        } else {
+                            logger.finest(toString() + "(networkSocketStillConnected) Network disconnected:");
+                        }
+                    }
+
+                    return pollResult;
+
+                } catch (SocketException se) {
+                    // Should never get here since the first one would have failed.
+                    if (logger.isLoggable(Level.FINE)) {
+                        logger.fine(
+                                toString() + "(networkSocketStillConnected) getSoTimeout failed:" + se.getMessage());
+                    }
+                    return false;
+                }
+            }
+        }
+    }
+
     final int read(byte[] data, int offset, int length) throws SQLServerException {
         try {
-            return inputStream.read(data, offset, length);
+            synchronized (inputStream) {
+                con.idleNetworkTracker.markNetworkActivity();
+                return inputStream.read(data, offset, length);
+            }
         } catch (IOException e) {
             if (logger.isLoggable(Level.FINE))
                 logger.fine(toString() + " read failed:" + e.getMessage());
@@ -2074,7 +2317,10 @@ final class TDSChannel implements Serializable {
 
     final void write(byte[] data, int offset, int length) throws SQLServerException {
         try {
-            outputStream.write(data, offset, length);
+            synchronized (outputStream) {
+                con.idleNetworkTracker.markNetworkActivity();
+                outputStream.write(data, offset, length);
+            }
         } catch (IOException e) {
             if (logger.isLoggable(Level.FINER))
                 logger.finer(toString() + " write failed:" + e.getMessage());
@@ -2085,6 +2331,7 @@ final class TDSChannel implements Serializable {
 
     final void flush() throws SQLServerException {
         try {
+            con.idleNetworkTracker.markNetworkActivity();
             outputStream.flush();
         } catch (IOException e) {
             if (logger.isLoggable(Level.FINER))
@@ -6873,6 +7120,30 @@ final class TDSReader implements Serializable {
         }
     }
 
+    /**
+     * This function reads valueLength no. of bytes from input buffer without storing them in any array
+     *
+     * @param valueLength
+     * @throws SQLServerException
+     */
+    final void readSkipBytes(long valueLength) throws SQLServerException {
+        for (long bytesSkipped = 0; bytesSkipped < valueLength;) {
+            // Ensure that we have a packet to read from.
+            if (!ensurePayload())
+                throwInvalidTDS();
+
+            long bytesToSkip = valueLength - bytesSkipped;
+            if (bytesToSkip > currentPacket.payloadLength - payloadOffset)
+                bytesToSkip = currentPacket.payloadLength - payloadOffset;
+
+            if (logger.isLoggable(Level.FINEST))
+                logger.finest(toString() + " Skipping " + bytesToSkip + " bytes from offset " + payloadOffset);
+
+            bytesSkipped += bytesToSkip;
+            payloadOffset += bytesToSkip;
+        }
+    }
+
     final byte[] readWrappedBytes(int valueLength) throws SQLServerException {
         assert valueLength <= valueBytes.length;
         readBytes(valueBytes, 0, valueLength);
@@ -7232,6 +7503,42 @@ final class TDSReader implements Serializable {
 }
 
 
+/**
+ * The tds default implementation of a timeout command
+ */
+class TdsTimeoutCommand extends TimeoutCommand<TDSCommand> {
+    protected TdsTimeoutCommand(int timeout, TDSCommand command, SQLServerConnection sqlServerConnection) {
+        super(timeout, command, sqlServerConnection);
+    }
+
+    protected void interrupt() throws Exception {
+        TDSCommand command = getCommand();
+        SQLServerConnection sqlServerConnection = getSqlServerConnection();
+        try {
+            // If TCP Connection to server is silently dropped, exceeding the query timeout
+            // on the same connection does
+            // not throw SQLTimeoutException
+            // The application stops responding instead until SocketTimeoutException is
+            // thrown. In this case, we must
+            // manually terminate the connection.
+            if (null == command && null != sqlServerConnection) {
+                sqlServerConnection.terminate(SQLServerException.DRIVER_ERROR_IO_FAILED,
+                        SQLServerException.getErrString("R_connectionIsClosed"));
+            } else {
+                // If the timer wasn't canceled before it ran out of
+                // time then interrupt the registered command.
+                command.interrupt(SQLServerException.getErrString("R_queryTimedOut"));
+            }
+        } catch (SQLServerException e) {
+            // Request failed to time out and SQLServerConnection does not exist
+            if (null != command)
+                command.log(Level.FINE, "Command could not be timed out. Reason: " + e.getMessage());
+            throw new Exception("Command could not be timed out. Reason: " + e.getMessage());
+        }
+    }
+}
+
+
 /**
  * TDSCommand encapsulates an interruptable TDS conversation.
  *
@@ -7359,6 +7666,14 @@ abstract class TDSCommand implements Serializable {
     private int queryTimeoutSeconds;
     private int cancelQueryTimeoutSeconds;
     private ScheduledFuture<?> timeout;
+    private TdsTimeoutCommand timeoutCommand;
+
+    /*
+     * Some flags for Connection Resiliency. We need to know if a command has already been registered in the poller, or
+     * if it was actually executed.
+     */
+    private boolean isRegisteredInPoller = false;
+    private boolean isExecuted = false;
 
     protected int getQueryTimeoutSeconds() {
         return this.queryTimeoutSeconds;
@@ -7391,6 +7706,22 @@ abstract class TDSCommand implements Serializable {
         }
     }
 
+    synchronized void addToPoller() {
+        if (!isRegisteredInPoller) {
+            // If command execution is subject to timeout then start timing until
+            // the server returns the first response packet.
+            if (queryTimeoutSeconds > 0) {
+                this.timeoutCommand = new TdsTimeoutCommand(queryTimeoutSeconds, this, null);
+                TimeoutPoller.getTimeoutPoller().addTimeoutCommand(this.timeoutCommand);
+                isRegisteredInPoller = true;
+            }
+        }
+    }
+
+    boolean wasExecuted() {
+        return isExecuted;
+    }
+
     /**
      * Creates this command with an optional timeout.
      *
@@ -7417,6 +7748,7 @@ abstract class TDSCommand implements Serializable {
      */
 
     boolean execute(TDSWriter tdsWriter, TDSReader tdsReader) throws SQLServerException {
+        isExecuted = true;
         this.tdsWriter = tdsWriter;
         this.tdsReader = tdsReader;
         assert null != tdsReader;
@@ -7574,12 +7906,16 @@ abstract class TDSCommand implements Serializable {
                 interruptReason = reason;
                 if (requestComplete)
                     attentionPending = tdsWriter.sendAttention();
-
+                if (correspondingThread != null) {
+                    this.correspondingThread.interrupt();
+                    this.correspondingThread = null;
+                }
             }
         }
     }
 
     private boolean interruptChecked = false;
+    private Thread correspondingThread = null;
 
     /**
      * Checks once whether an interrupt has occurred, and, if it has, throws an exception indicating that fact.
@@ -7797,6 +8133,7 @@ abstract class TDSCommand implements Serializable {
             SQLServerConnection conn = tdsReader != null ? tdsReader.getConnection() : null;
             this.timeout = tdsWriter.getSharedTimer().schedule(new TDSTimeoutTask(this, conn), queryTimeoutSeconds);
         }
+        addToPoller();
 
         if (logger.isLoggable(Level.FINEST))
             logger.finest(this.toString() + ": Reading response...");
@@ -7823,9 +8160,18 @@ abstract class TDSCommand implements Serializable {
                 this.timeout = null;
             }
         }
-
+        // A new response is received hence increment unprocessed response count.
+        tdsReader.getConnection().getSessionRecovery().incrementUnprocessedResponseCount();
         return tdsReader;
     }
+
+    /*
+     * Currently only used in Connection Resiliency scenarios. This thread reference allows the current command to
+     * interrupt the thread if it's sleeping. This is useful in timeout cases.
+     */
+    void attachThread(Thread reconnectThread) {
+        this.correspondingThread = reconnectThread;
+    }
 }
 
 
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection43.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection43.java
index 9eb725de..97da9714 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection43.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection43.java
@@ -36,7 +36,6 @@ public interface ISQLServerConnection43 extends ISQLServerConnection {
      * <p>
      * The pooling manager should call {@code beginRequest} on the underlying connection prior to returning a connection
      * to the caller.
-     * <p>
      * 
      * @throws SQLException
      *         if an error occurs
@@ -52,7 +51,6 @@ public interface ISQLServerConnection43 extends ISQLServerConnection {
      * without an intervening call to {@code beginRequest} is not an error. The first {@code endRequest} call marks the
      * request completed and subsequent calls are treated as a no-op. If {@code endRequest} is called without an initial
      * call to {@code beginRequest} is a no-op. This method is to be used by Connection pooling managers.
-     * <p>
      * 
      * @throws SQLException
      *         if an error occurs
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
index 443f8c75..a74a818c 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
@@ -1098,34 +1098,32 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource {
     void setMaxResultBuffer(String maxResultBuffer);
 
     /**
-     * Sets the 'connectRetryCount' property that specifies the number of reconnection attempts if there is a connection
-     * failure
-     * 
-     * @param count
-     *        the number of reconnection attempts if there is a connection failure
+     * Sets the maximum number of attempts to reestablish a broken connection.
+     *
+     * @param connectRetryCount
+     *        maximum number of attempts
      */
-    void setConnectRetryCount(int count);
+    void setConnectRetryCount(int connectRetryCount);
 
     /**
-     * Returns the value of 'connectRetryCount' property
-     * 
-     * @return 'connectRetryCount' property.
+     * Returns the maximum number of attempts set to reestablish a broken connection.
+     *
+     * @return maximum number of attempts
      */
     int getConnectRetryCount();
 
     /**
-     * Sets the 'connectRetryInterval' property that specifies the number of seconds between each connection retry
-     * attempt
-     * 
-     * @param interval
-     *        the number of seconds between each connection retry attempt
+     * Sets the interval, in seconds, between attempts to reestablish a broken connection.
+     *
+     * @param connectRetryInterval
+     *        interval in seconds
      */
-    void setConnectRetryInterval(int interval);
+    void setConnectRetryInterval(int connectRetryInterval);
 
     /**
-     * Returns the value of 'connectRetryInterval' property
-     * 
-     * @return 'connectRetryInterval' property.
+     * Returns the interval set, in seconds, between attempts to reestablish a broken connection.
+     *
+     * @return interval in seconds
      */
     int getConnectRetryInterval();
 }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IdleConnectionResiliency.java b/src/main/java/com/microsoft/sqlserver/jdbc/IdleConnectionResiliency.java
new file mode 100644
index 00000000..43dae202
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/IdleConnectionResiliency.java
@@ -0,0 +1,470 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+
+package com.microsoft.sqlserver.jdbc;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+
+class IdleConnectionResiliency {
+    private boolean connectionRecoveryNegotiated;
+    private int connectRetryCount;
+    private SQLServerConnection connection;
+    private SessionStateTable sessionStateTable;
+    private ReconnectThread reconnectThread;
+    private AtomicInteger unprocessedResponseCount = new AtomicInteger();
+    private boolean connectionRecoveryPossible;
+
+    /*
+     * Variables needed to perform a reconnect, these are not necessarily determined from just the connection string
+     */
+    private String loginInstanceValue;
+    private int loginNPort;
+    private FailoverInfo loginFailoverInfo;
+    private int loginLoginTimeoutSeconds;
+
+    IdleConnectionResiliency(SQLServerConnection connection) {
+        this.connection = connection;
+        reconnectThread = new ReconnectThread(connection);
+    }
+
+    boolean isConnectionRecoveryNegotiated() {
+        return connectionRecoveryNegotiated;
+    }
+
+    void setConnectionRecoveryNegotiated(boolean connectionRecoveryNegotiated) {
+        this.connectionRecoveryNegotiated = connectionRecoveryNegotiated;
+    }
+
+    int getConnectRetryCount() {
+        return connectRetryCount;
+    }
+
+    void setConnectRetryCount(int connectRetryCount) {
+        this.connectRetryCount = connectRetryCount;
+    }
+
+    SQLServerConnection getConnection() {
+        return connection;
+    }
+
+    void setConnection(SQLServerConnection connection) {
+        this.connection = connection;
+    }
+
+    ReconnectThread getReconnectThread() {
+        return reconnectThread;
+    }
+
+    SessionStateTable getSessionStateTable() {
+        return sessionStateTable;
+    }
+
+    void setSessionStateTable(SessionStateTable sessionStateTable) {
+        this.sessionStateTable = sessionStateTable;
+    }
+
+    boolean isConnectionRecoveryPossible() {
+        return connectionRecoveryPossible;
+    }
+
+    void setConnectionRecoveryPossible(boolean connectionRecoveryPossible) {
+        this.connectionRecoveryPossible = connectionRecoveryPossible;
+    }
+
+    int getUnprocessedResponseCount() {
+        return unprocessedResponseCount.get();
+    }
+
+    void resetUnprocessedResponseCount() {
+        this.unprocessedResponseCount.set(0);
+    }
+
+    void parseInitialSessionStateData(byte[] data, byte[][] sessionStateInitial) throws SQLServerException {
+        int bytesRead = 0;
+
+        // Contains StateId, StateLen, StateValue
+        while (bytesRead < data.length) {
+            short sessionStateId = (short) (data[bytesRead] & 0xFF);
+            bytesRead++;
+            int sessionStateLength;
+            byte byteLength = data[bytesRead];
+            bytesRead++;
+            if (byteLength == 0xFF) {
+                sessionStateLength = (int) (Util.readInt(data, bytesRead) & 0xFFFFFFFFL);
+                bytesRead += 4;
+            } else {
+                sessionStateLength = byteLength;
+            }
+            sessionStateInitial[sessionStateId] = new byte[sessionStateLength];
+            System.arraycopy(data, bytesRead, sessionStateInitial[sessionStateId], 0, sessionStateLength);
+            bytesRead += sessionStateLength;
+        }
+    }
+
+    void incrementUnprocessedResponseCount() {
+        if (connection.getRetryCount() > 0 && !getReconnectThread().isAlive()) {
+            if (unprocessedResponseCount.incrementAndGet() < 0)
+                /*
+                 * When this number rolls over, connection recovery is disabled for the rest of the life of the
+                 * connection.
+                 */
+                setConnectionRecoveryPossible(false);
+        }
+    }
+
+    void decrementUnprocessedResponseCount() {
+        if (connection.getRetryCount() > 0 && !getReconnectThread().isAlive()) {
+            if (unprocessedResponseCount.decrementAndGet() < 0)
+                /*
+                 * When this number rolls over, connection recovery is disabled for the rest of the life of the
+                 * connection.
+                 */
+                setConnectionRecoveryPossible(false);
+        }
+    }
+
+    void setLoginParameters(String instanceValue, int nPort, FailoverInfo fo, int loginTimeoutSeconds) {
+        this.loginInstanceValue = instanceValue;
+        this.loginNPort = nPort;
+        this.loginFailoverInfo = fo;
+        this.loginLoginTimeoutSeconds = loginTimeoutSeconds;
+    }
+
+    String getInstanceValue() {
+        return loginInstanceValue;
+    }
+
+    int getNPort() {
+        return loginNPort;
+    }
+
+    FailoverInfo getFailoverInfo() {
+        return loginFailoverInfo;
+    }
+
+    int getLoginTimeoutSeconds() {
+        return loginLoginTimeoutSeconds;
+    }
+}
+
+
+class SessionStateValue {
+    private boolean isRecoverable;
+    private int sequenceNumber;
+    private int dataLength;
+    private byte[] data;
+
+    boolean isSequenceNumberGreater(int sequenceNumberToBeCompared) {
+        // Illustration using 8 bit number
+
+        // Initial assignment takes care of following scenarios:
+        // toBeCompared= 2 benchmark = 1 (both positive) ..true
+        // toBeCompared=-1(255) benchmark = -2(254) (both negative) ..true
+        // toBeCompared=-1(255) benchmark = 1 ..true
+        // toBeCompared=-1(255) benchmark = 0 ..true
+        boolean greater = true;
+
+        if (sequenceNumberToBeCompared > sequenceNumber) {
+            // Following if condition takes care of these scenarios:
+            // toBeCompared = 0 benchmark = -1(255)
+            // toBeCompared = 1 benchmark = -1(255)
+            if ((sequenceNumberToBeCompared >= 0) && (sequenceNumber < 0))
+                greater = false;
+        }
+        // This else takes care of these scenarios where result is false:
+        // toBeCompared= 1 benchmark = 2 (both positive) ..false
+        // toBeCompared=-2(254) benchmark = -1(255) (both negative) ..false
+        else
+        // Following if condition to not set return to false for these scenarios:
+        // toBeCompared=-1(255) benchmark = 1 ..true
+        // toBeCompared=-1(255) benchmark = 0 ..true
+        if ((sequenceNumberToBeCompared > 0) || (sequenceNumber < 0))
+            greater = false;
+
+        return greater;
+    }
+
+    boolean isRecoverable() {
+        return isRecoverable;
+    }
+
+    void setRecoverable(boolean isRecoverable) {
+        this.isRecoverable = isRecoverable;
+    }
+
+    int getSequenceNumber() {
+        return sequenceNumber;
+    }
+
+    void setSequenceNumber(int sequenceNumber) {
+        this.sequenceNumber = sequenceNumber;
+    }
+
+    int getDataLength() {
+        return dataLength;
+    }
+
+    void setDataLengh(int dataLength) {
+        this.dataLength = dataLength;
+    }
+
+    byte[] getData() {
+        return data;
+    }
+
+    void setData(byte[] data) {
+        this.data = data;
+    }
+}
+
+
+class SessionStateTable {
+    static final int SESSION_STATE_ID_MAX = 256;
+    static final long MASTER_RECOVERY_DISABLE_SEQ_NUMBER = 0XFFFFFFFF;
+    private boolean masterRecoveryDisabled;
+    private byte[][] sessionStateInitial;
+    private SessionStateValue sessionStateDelta[];
+    private AtomicInteger unRecoverableSessionStateCount = new AtomicInteger(0);
+    private String originalCatalog;
+    private String originalLanguage;
+    private SQLCollation originalCollation;
+    private byte originalNegotiatedEncryptionLevel = TDS.ENCRYPT_INVALID;
+    private boolean resetCalled = false;
+
+    SessionStateTable() {
+        this.sessionStateDelta = new SessionStateValue[SESSION_STATE_ID_MAX];
+        this.sessionStateInitial = new byte[SessionStateTable.SESSION_STATE_ID_MAX][];
+    }
+
+    void updateSessionState(TDSReader tdsReader, short sessionStateId, int sessionStateLength, int sequenceNumber,
+            boolean fRecoverable) throws SQLServerException {
+        sessionStateDelta[sessionStateId].setSequenceNumber(sequenceNumber);
+        sessionStateDelta[sessionStateId].setDataLengh(sessionStateLength);
+
+        if ((sessionStateDelta[sessionStateId].getData() == null)
+                || (sessionStateDelta[sessionStateId].getData().length < sessionStateLength)) {
+            sessionStateDelta[sessionStateId].setData(new byte[sessionStateLength]);
+
+            // First time state update and value is not recoverable, hence count is incremented.
+            if (!fRecoverable) {
+                unRecoverableSessionStateCount.incrementAndGet();
+            }
+        } else {
+            // Not a first time state update hence if only there is a transition in state do we update the count.
+            if (fRecoverable != sessionStateDelta[sessionStateId].isRecoverable()) {
+                if(fRecoverable)
+                    unRecoverableSessionStateCount.decrementAndGet();
+                else
+                    unRecoverableSessionStateCount.incrementAndGet();
+            }
+        }
+        tdsReader.readBytes(sessionStateDelta[sessionStateId].getData(), 0, sessionStateLength);
+        sessionStateDelta[sessionStateId].setRecoverable(fRecoverable);
+    }
+
+    /**
+     * @return length of initial session state data.
+     */
+    int getInitialLength() {
+        int length = 0;
+        for (int i = 0; i < SESSION_STATE_ID_MAX; i++) {
+            if (sessionStateInitial[i] != null) {
+                length += (1/* state id */ + (sessionStateInitial[i].length < 0xFF ? 1 : 5)/* Data length */
+                        + sessionStateInitial[i].length);
+            }
+        }
+        return length;
+    }
+
+    /**
+     * @return length of delta session state data.
+     */
+    int getDeltaLength() {
+        int length = 0;
+        for (int i = 0; i < SESSION_STATE_ID_MAX; i++) {
+            if (sessionStateDelta[i] != null && sessionStateDelta[i].getData() != null) {
+                length += (1/* state id */ + (sessionStateDelta[i].getDataLength() < 0xFF ? 1 : 5)/* Data length */
+                        + sessionStateDelta[i].getDataLength());
+            }
+        }
+        return length;
+    }
+
+    boolean isSessionRecoverable() {
+        return (!isMasterRecoveryDisabled() && (0 == unRecoverableSessionStateCount.get()));
+    }
+
+    boolean isMasterRecoveryDisabled() {
+        return masterRecoveryDisabled;
+    }
+
+    void setMasterRecoveryDisabled(boolean masterRecoveryDisabled) {
+        this.masterRecoveryDisabled = masterRecoveryDisabled;
+    }
+
+    byte[][] getSessionStateInitial() {
+        return sessionStateInitial;
+    }
+
+    void setSessionStateInitial(byte[][] sessionStateInitial) {
+        this.sessionStateInitial = sessionStateInitial;
+    }
+
+    SessionStateValue[] getSessionStateDelta() {
+        return sessionStateDelta;
+    }
+
+    void setSessionStateDelta(SessionStateValue[] sessionStateDelta) {
+        this.sessionStateDelta = sessionStateDelta;
+    }
+
+    String getOriginalCatalog() {
+        return originalCatalog;
+    }
+
+    void setOriginalCatalog(String catalog) {
+        this.originalCatalog = catalog;
+    }
+
+    String getOriginalLanguage() {
+        return originalLanguage;
+    }
+
+    void setOriginalLanguage(String language) {
+        this.originalLanguage = language;
+    }
+
+    SQLCollation getOriginalCollation() {
+        return originalCollation;
+    }
+
+    void setOriginalCollation(SQLCollation collation) {
+        this.originalCollation = collation;
+    }
+
+    byte getOriginalNegotiatedEncryptionLevel() {
+        return originalNegotiatedEncryptionLevel;
+    }
+
+    void setOriginalNegotiatedEncryptionLevel(byte originalNegotiatedEncryptionLevel) {
+        this.originalNegotiatedEncryptionLevel = originalNegotiatedEncryptionLevel;
+    }
+
+    boolean spResetCalled() {
+        return resetCalled;
+    }
+
+    void setspResetCalled(boolean status) {
+        this.resetCalled = status;
+    }
+
+    public void reset() {
+        resetCalled = true;
+        sessionStateDelta = new SessionStateValue[SESSION_STATE_ID_MAX];
+        unRecoverableSessionStateCount = new AtomicInteger(0);
+    }
+}
+
+
+final class ReconnectThread extends Thread {
+    private SQLServerConnection con = null;
+    private SQLServerException eReceived = null;
+    private TDSCommand command = null;
+
+    private volatile boolean stopRequested = false;
+    private int connectRetryCount = 0;
+
+    /*
+     * This class is only meant to be used by a Connection object to reconnect in the background. Don't allow default
+     * instantiation as it doesn't make sense.
+     */
+    @SuppressWarnings("unused")
+    private ReconnectThread() {};
+
+    ReconnectThread(SQLServerConnection sqlC) {
+        this.con = sqlC;
+    }
+
+    // Resets the thread
+    void init(TDSCommand cmd) {
+        this.command = cmd;
+        connectRetryCount = con.getRetryCount();
+        eReceived = null;
+        stopRequested = false;
+    }
+
+    public void run() {
+        boolean interruptsEnabled = command.getInterruptsEnabled();
+        /*
+         * All TDSCommands are not interruptible before execution, and all the commands passed to here won't have been
+         * executed. We need to be able to interrupt these commands so the TimeoutPoller can tell us when a query has
+         * timed out.
+         */
+        command.setInterruptsEnabled(true);
+        command.attachThread(this);
+        command.addToPoller();
+        boolean keepRetrying = true;
+
+        while ((connectRetryCount != 0) && (!stopRequested) && keepRetrying) {
+            try {
+                eReceived = null;
+                con.connect(null, con.getPooledConnectionParent());
+                keepRetrying = false;
+            } catch (SQLServerException e) {
+                if (!stopRequested) {
+                    eReceived = e;
+                    if (con.isFatalError(e)) {
+                        keepRetrying = false;
+                    } else {
+                        try {
+                            if (connectRetryCount > 1) {
+                                Thread.sleep(con.getRetryInterval() * 1000);
+                            }
+                        } catch (InterruptedException ie) {
+                            this.eReceived = new SQLServerException(SQLServerException.getErrString("R_queryTimedOut"),
+                                    SQLState.STATEMENT_CANCELED, DriverError.NOT_SET, null);
+                            keepRetrying = false;
+                        }
+                    }
+                }
+            } finally {
+                connectRetryCount--;
+                try {
+                    command.checkForInterrupt();
+                } catch (SQLServerException e) {
+                    // Interrupted, timeout occurred. Stop retrying.
+                    keepRetrying = false;
+                    eReceived = e;
+                }
+            }
+        }
+
+        if ((connectRetryCount == 0) && (keepRetrying)) {
+            eReceived = new SQLServerException(SQLServerException.getErrString("R_crClientAllRecoveryAttemptsFailed"),
+                    eReceived);
+        }
+
+        command.setInterruptsEnabled(interruptsEnabled);
+        return;
+    }
+
+    void stop(boolean blocking) {
+        stopRequested = true;
+        if (blocking && this.isAlive()) {
+            while (this.getState() != State.TERMINATED) {
+                // wait until thread terminates
+            }
+        }
+    }
+
+    /*
+     * Run method can not be implemented to return an exception hence statement execution thread that called
+     * reconnection will get exception through this function as soon as reconnection thread execution is over.
+     */
+    SQLServerException getException() {
+        return eReceived;
+    }
+}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLCollation.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLCollation.java
index 75a1aed7..59ace925 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLCollation.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLCollation.java
@@ -75,6 +75,10 @@ final class SQLCollation implements java.io.Serializable {
         return this.sortId;
     }
 
+    boolean isEqual(SQLCollation col) {
+        return ((col != null && col.info == info && col.sortId == sortId) ? true : false);
+    }
+
     /**
      * Reads TDS collation from TDS buffer into SQLCollation class.
      * 
@@ -616,6 +620,10 @@ enum Encoding {
         return charset;
     }
 
+    String getCharsetName() {
+        return charsetName;
+    }
+
     /**
      * Returns true if the collation supports conversion to ascii.
      *
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java
index 095a526d..dd6343de 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java
@@ -255,6 +255,10 @@ public class SQLServerCallableStatement extends SQLServerPreparedStatement imple
                 // Consume the done token and decide what to do with it...
                 StreamDone doneToken = new StreamDone();
                 doneToken.setFromTDS(tdsReader);
+                if (doneToken.isFinal()) {
+                    // Response is completely processed, hence decrement unprocessed response count.
+                    connection.getSessionRecovery().decrementUnprocessedResponseCount();
+                }
 
                 // If this is a non-final batch-terminating DONE token,
                 // then stop parsing the response now and set up for
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
index 3f5730c7..b12facec 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
@@ -29,6 +29,7 @@ import java.sql.Savepoint;
 import java.sql.Statement;
 import java.text.MessageFormat;
 import java.time.Duration;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Enumeration;
@@ -377,6 +378,41 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         }
     }
 
+    /**
+     * Keeps track of the last network activity in order to tell if the connection has been idle. This is done to avoid
+     * checking the socket for a disconnect every command execution, which has a high impact on perf.
+     */
+    class IdleNetworkTracker {
+        private Instant lastNetworkActivity = Instant.now();
+
+        /**
+         * An “idle” connection will only ever get its socket disconnected by a keepalive packet after a connection has
+         * been severed. KeepAlive packets are only sent on idle sockets. Default setting by the driver (on platforms
+         * that have Java support for setting it) and the recommended setting is 30s (and OS default for those that
+         * don't set it is 2 hrs).
+         */
+        private int maxIdleMillis = 15000;
+
+        /** Has it been more than maxIdleMillis since network activity has been marked */
+        private boolean isIdle() {
+            boolean ret = Instant.now().minusMillis(maxIdleMillis).isAfter(lastNetworkActivity);
+            return ret;
+        }
+
+        /** Mark network activity now */
+        protected void markNetworkActivity() {
+            lastNetworkActivity = Instant.now();
+        }
+
+        /** Set max idle time in milliseconds */
+        protected void setMaxIdleMillis(int millis) {
+            maxIdleMillis = millis;
+        }
+    }
+
+    /** Keeps track of network idle status */
+    IdleNetworkTracker idleNetworkTracker = new IdleNetworkTracker();
+
     /** Size of the parsed SQL-text metadata cache */
     static final private int PARSED_SQL_CACHE_SIZE = 100;
 
@@ -827,6 +863,15 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         delayLoadingLobs = b;
     }
 
+    /** Session Recovery Object */
+    private IdleConnectionResiliency sessionRecovery = new IdleConnectionResiliency(this);
+
+    IdleConnectionResiliency getSessionRecovery() {
+        return sessionRecovery;
+    }
+
+    static boolean isWindows;
+
     /** global system ColumnEncryptionKeyStoreProviders */
     static Map<String, SQLServerColumnEncryptionKeyStoreProvider> globalSystemColumnEncryptionKeyStoreProviders = new HashMap<>();
 
@@ -883,8 +928,8 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
 
             SQLServerColumnEncryptionKeyStoreProvider provider = entry.getValue();
             if (null == provider) {
-                throw new SQLServerException(null,
-                        String.format(SQLServerException.getErrString("R_CustomKeyStoreProviderValueNull"), providerName),
+                throw new SQLServerException(null, String
+                        .format(SQLServerException.getErrString("R_CustomKeyStoreProviderValueNull"), providerName),
                         null, 0, false);
             }
 
@@ -949,13 +994,12 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
 
     synchronized SQLServerColumnEncryptionKeyStoreProvider getSystemOrGlobalColumnEncryptionKeyStoreProvider(
             String providerName) throws SQLServerException {
-        
+
         // check for global system providers
         keystoreProvider = getGlobalSystemColumnEncryptionKeyStoreProvider(providerName);
 
         // Check for the connection system provider.
-        if (null == keystoreProvider)
-        {
+        if (null == keystoreProvider) {
             keystoreProvider = getSystemColumnEncryptionKeyStoreProvider(providerName);
         }
 
@@ -978,7 +1022,8 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
     }
 
     synchronized boolean hasConnectionColumnEncryptionKeyStoreProvidersRegistered() {
-        return null != connectionColumnEncryptionKeyStoreProvider && connectionColumnEncryptionKeyStoreProvider.size() > 0;
+        return null != connectionColumnEncryptionKeyStoreProvider
+                && connectionColumnEncryptionKeyStoreProvider.size() > 0;
     }
 
     synchronized String getAllConnectionColumnEncryptionKeyStoreProviders() {
@@ -992,8 +1037,8 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
             String providerName) throws SQLServerException {
 
         // Check for a connection-level provider first
-        if (null != connectionColumnEncryptionKeyStoreProvider &&
-                connectionColumnEncryptionKeyStoreProvider.size() > 0) {
+        if (null != connectionColumnEncryptionKeyStoreProvider
+                && connectionColumnEncryptionKeyStoreProvider.size() > 0) {
             // If any connection-level providers are registered, we don't fall back to system/global providers
             if (connectionColumnEncryptionKeyStoreProvider.containsKey(providerName)) {
                 return connectionColumnEncryptionKeyStoreProvider.get(providerName);
@@ -1048,10 +1093,10 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
             }
 
             if (null == entry.getValue()) {
-                throw new SQLServerException(null,
-                        String.format(SQLServerException.getErrString("R_CustomKeyStoreProviderValueNull"), providerName),
+                throw new SQLServerException(null, String
+                        .format(SQLServerException.getErrString("R_CustomKeyStoreProviderValueNull"), providerName),
                         null, 0, false);
-            }            
+            }
 
             connectionColumnEncryptionKeyStoreProvider.put(entry.getKey(), entry.getValue());
         }
@@ -1293,6 +1338,9 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
     /** This is the catalog immediately after login. */
     private String originalCatalog = "master";
 
+    /** This is the current language */
+    private String sLanguage = "us_english";
+
     /** transaction isolation level */
     private int transactionIsolationLevel;
 
@@ -1309,6 +1357,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
             .getLogger("com.microsoft.sqlserver.jdbc.internals.SQLServerConnection");
     static final private java.util.logging.Logger loggerExternal = java.util.logging.Logger
             .getLogger("com.microsoft.sqlserver.jdbc.Connection");
+
     private static String loggingClassNameBase = "com.microsoft.sqlserver.jdbc.SQLServerConnection";
 
     /** Instance-specific loggingClassName to identity the connection in logs */
@@ -1388,10 +1437,22 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         return clientConnectionId;
     }
 
+    final int getRetryInterval() {
+        return connectRetryInterval;
+    }
+
+    final int getRetryCount() {
+        return connectRetryCount;
+    }
+
     final boolean attachConnId() {
         return state.equals(State.Connected);
     }
 
+    SQLServerPooledConnection getPooledConnectionParent() {
+        return pooledConnectionParent;
+    }
+
     SQLServerConnection(String parentInfo) throws SQLServerException {
         int connectionID = nextConnectionID(); // sequential connection id
         traceID = "ConnectionID:" + connectionID;
@@ -1533,18 +1594,20 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
 
     Connection connect(Properties propsIn, SQLServerPooledConnection pooledConnection) throws SQLServerException {
         int loginTimeoutSeconds = SQLServerDriverIntProperty.LOGIN_TIMEOUT.getDefaultValue();
-        String sPropValue = propsIn.getProperty(SQLServerDriverIntProperty.LOGIN_TIMEOUT.toString());
-        try {
-            if (null != sPropValue && sPropValue.length() > 0) {
-                int sPropValueInt = Integer.parseInt(sPropValue);
-                if (0 != sPropValueInt) { // Use the default timeout in case of a zero value
-                    loginTimeoutSeconds = sPropValueInt;
+        if (propsIn != null) {
+            String sPropValue = propsIn.getProperty(SQLServerDriverIntProperty.LOGIN_TIMEOUT.toString());
+            try {
+                if (null != sPropValue && sPropValue.length() > 0) {
+                    int sPropValueInt = Integer.parseInt(sPropValue);
+                    if (0 != sPropValueInt) { // Use the default timeout in case of a zero value
+                        loginTimeoutSeconds = sPropValueInt;
+                    }
                 }
+            } catch (NumberFormatException e) {
+                MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidTimeOut"));
+                Object[] msgArgs = {sPropValue};
+                SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
             }
-        } catch (NumberFormatException e) {
-            MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidTimeOut"));
-            Object[] msgArgs = {sPropValue};
-            SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
         }
 
         long elapsedSeconds = 0;
@@ -1733,840 +1796,906 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
     Connection connectInternal(Properties propsIn,
             SQLServerPooledConnection pooledConnection) throws SQLServerException {
         try {
-            activeConnectionProperties = (Properties) propsIn.clone();
+            if (propsIn != null) {
 
-            pooledConnectionParent = pooledConnection;
+                activeConnectionProperties = (Properties) propsIn.clone();
 
-            String hostNameInCertificate = activeConnectionProperties
-                    .getProperty(SQLServerDriverStringProperty.HOSTNAME_IN_CERTIFICATE.toString());
+                pooledConnectionParent = pooledConnection;
 
-            /*
-             * hostNameInCertificate property can change when redirection is involved, so maintain this value for every
-             * instance of SQLServerConnection.
-             */
-            if (null == originalHostNameInCertificate && null != hostNameInCertificate
-                    && !hostNameInCertificate.isEmpty()) {
-                originalHostNameInCertificate = activeConnectionProperties
+                String hostNameInCertificate = activeConnectionProperties
                         .getProperty(SQLServerDriverStringProperty.HOSTNAME_IN_CERTIFICATE.toString());
-            }
-
-            /*
-             * if hostNameInCertificate has a legitimate value (and not empty or null), reset hostNameInCertificate to
-             * the original value every time we connect (or re-connect).
-             */
-            if (null != originalHostNameInCertificate && !originalHostNameInCertificate.isEmpty()) {
-                activeConnectionProperties.setProperty(SQLServerDriverStringProperty.HOSTNAME_IN_CERTIFICATE.toString(),
-                        originalHostNameInCertificate);
-            }
 
-            String sPropKey;
-            String sPropValue;
+                /*
+                 * hostNameInCertificate property can change when redirection is involved, so maintain this value for
+                 * every instance of SQLServerConnection.
+                 */
+                if (null == originalHostNameInCertificate && null != hostNameInCertificate
+                        && !hostNameInCertificate.isEmpty()) {
+                    originalHostNameInCertificate = activeConnectionProperties
+                            .getProperty(SQLServerDriverStringProperty.HOSTNAME_IN_CERTIFICATE.toString());
+                }
 
-            sPropKey = SQLServerDriverStringProperty.USER.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null == sPropValue) {
-                sPropValue = SQLServerDriverStringProperty.USER.getDefaultValue();
-                activeConnectionProperties.setProperty(sPropKey, sPropValue);
-            }
-            validateMaxSQLLoginName(sPropKey, sPropValue);
+                /*
+                 * if hostNameInCertificate has a legitimate value (and not empty or null), reset hostNameInCertificate
+                 * to the original value every time we connect (or re-connect).
+                 */
+                if (null != originalHostNameInCertificate && !originalHostNameInCertificate.isEmpty()) {
+                    activeConnectionProperties.setProperty(
+                            SQLServerDriverStringProperty.HOSTNAME_IN_CERTIFICATE.toString(),
+                            originalHostNameInCertificate);
+                }
 
-            sPropKey = SQLServerDriverStringProperty.PASSWORD.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null == sPropValue) {
-                sPropValue = SQLServerDriverStringProperty.PASSWORD.getDefaultValue();
-                activeConnectionProperties.setProperty(sPropKey, sPropValue);
-            }
-            validateMaxSQLLoginName(sPropKey, sPropValue);
+                String sPropKey;
+                String sPropValue;
 
-            sPropKey = SQLServerDriverStringProperty.DATABASE_NAME.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            validateMaxSQLLoginName(sPropKey, sPropValue);
+                sPropKey = SQLServerDriverStringProperty.USER.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = SQLServerDriverStringProperty.USER.getDefaultValue();
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                }
+                validateMaxSQLLoginName(sPropKey, sPropValue);
 
-            // if the user does not specify a default timeout, default is 15 per spec
-            int loginTimeoutSeconds = validateTimeout(SQLServerDriverIntProperty.LOGIN_TIMEOUT);
+                sPropKey = SQLServerDriverStringProperty.PASSWORD.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = SQLServerDriverStringProperty.PASSWORD.getDefaultValue();
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                }
+                validateMaxSQLLoginName(sPropKey, sPropValue);
 
-            connectRetryCount = validateTimeout(SQLServerDriverIntProperty.CONNECT_RETRY_COUNT);
-            connectRetryInterval = validateTimeout(SQLServerDriverIntProperty.CONNECT_RETRY_INTERVAL);
+                sPropKey = SQLServerDriverStringProperty.DATABASE_NAME.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                validateMaxSQLLoginName(sPropKey, sPropValue);
 
-            // Translates the serverName from Unicode to ASCII Compatible Encoding (ACE), as defined by the ToASCII
-            // operation of RFC 3490.
-            sPropKey = SQLServerDriverBooleanProperty.SERVER_NAME_AS_ACE.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null == sPropValue) {
-                sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.SERVER_NAME_AS_ACE.getDefaultValue());
-                activeConnectionProperties.setProperty(sPropKey, sPropValue);
-            }
-            serverNameAsACE = isBooleanPropertyOn(sPropKey, sPropValue);
+                // if the user does not specify a default timeout, default is 15 per spec
+                int loginTimeoutSeconds = validateTimeout(SQLServerDriverIntProperty.LOGIN_TIMEOUT);
 
-            // get the server name from the properties if it has instance name in it, getProperty the instance name
-            // if there is a port number specified do not get the port number from the instance name
-            sPropKey = SQLServerDriverStringProperty.SERVER_NAME.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                // Translates the serverName from Unicode to ASCII Compatible Encoding (ACE), as defined by the ToASCII
+                // operation of RFC 3490.
+                sPropKey = SQLServerDriverBooleanProperty.SERVER_NAME_AS_ACE.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.SERVER_NAME_AS_ACE.getDefaultValue());
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                }
+                serverNameAsACE = isBooleanPropertyOn(sPropKey, sPropValue);
 
-            if (null == sPropValue) {
-                sPropValue = "localhost";
-            }
+                // get the server name from the properties if it has instance name in it, getProperty the instance name
+                // if there is a port number specified do not get the port number from the instance name
+                sPropKey = SQLServerDriverStringProperty.SERVER_NAME.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
 
-            String sPropKeyPort = SQLServerDriverIntProperty.PORT_NUMBER.toString();
-            String sPropValuePort = activeConnectionProperties.getProperty(sPropKeyPort);
+                if (null == sPropValue) {
+                    sPropValue = "localhost";
+                }
 
-            int px = sPropValue.indexOf('\\');
+                String sPropKeyPort = SQLServerDriverIntProperty.PORT_NUMBER.toString();
+                String sPropValuePort = activeConnectionProperties.getProperty(sPropKeyPort);
 
-            String instanceValue = null;
+                int px = sPropValue.indexOf('\\');
 
-            String instanceNameProperty = SQLServerDriverStringProperty.INSTANCE_NAME.toString();
-            // found the instance name with the servername
-            if (px >= 0) {
-                instanceValue = sPropValue.substring(px + 1, sPropValue.length());
-                validateMaxSQLLoginName(instanceNameProperty, instanceValue);
-                sPropValue = sPropValue.substring(0, px);
-            }
-            trustedServerNameAE = sPropValue;
+                String instanceValue = null;
 
-            if (serverNameAsACE) {
-                try {
-                    sPropValue = java.net.IDN.toASCII(sPropValue);
-                } catch (IllegalArgumentException ex) {
-                    MessageFormat form = new MessageFormat(
-                            SQLServerException.getErrString("R_InvalidConnectionSetting"));
-                    Object[] msgArgs = {"serverNameAsACE", sPropValue};
-                    throw new SQLServerException(form.format(msgArgs), ex);
+                String instanceNameProperty = SQLServerDriverStringProperty.INSTANCE_NAME.toString();
+                // found the instance name with the servername
+                if (px >= 0) {
+                    instanceValue = sPropValue.substring(px + 1, sPropValue.length());
+                    validateMaxSQLLoginName(instanceNameProperty, instanceValue);
+                    sPropValue = sPropValue.substring(0, px);
                 }
-            }
-            activeConnectionProperties.setProperty(sPropKey, sPropValue);
-
-            String instanceValueFromProp = activeConnectionProperties.getProperty(instanceNameProperty);
-            // property takes precedence
-            if (null != instanceValueFromProp)
-                instanceValue = instanceValueFromProp;
-
-            if (instanceValue != null) {
-                validateMaxSQLLoginName(instanceNameProperty, instanceValue);
-                // only get port if the port is not specified
-                activeConnectionProperties.setProperty(instanceNameProperty, instanceValue);
-                trustedServerNameAE += "\\" + instanceValue;
-            }
-
-            if (null != sPropValuePort) {
-                trustedServerNameAE += ":" + sPropValuePort;
-            }
+                trustedServerNameAE = sPropValue;
 
-            sPropKey = SQLServerDriverStringProperty.APPLICATION_NAME.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null != sPropValue)
-                validateMaxSQLLoginName(sPropKey, sPropValue);
-            else
-                activeConnectionProperties.setProperty(sPropKey, SQLServerDriver.DEFAULT_APP_NAME);
-
-            sPropKey = SQLServerDriverBooleanProperty.LAST_UPDATE_COUNT.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null == sPropValue) {
-                sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.LAST_UPDATE_COUNT.getDefaultValue());
+                if (serverNameAsACE) {
+                    try {
+                        sPropValue = java.net.IDN.toASCII(sPropValue);
+                    } catch (IllegalArgumentException ex) {
+                        MessageFormat form = new MessageFormat(
+                                SQLServerException.getErrString("R_InvalidConnectionSetting"));
+                        Object[] msgArgs = {"serverNameAsACE", sPropValue};
+                        throw new SQLServerException(form.format(msgArgs), ex);
+                    }
+                }
                 activeConnectionProperties.setProperty(sPropKey, sPropValue);
-            }
 
-            sPropKey = SQLServerDriverStringProperty.COLUMN_ENCRYPTION.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null == sPropValue) {
-                sPropValue = SQLServerDriverStringProperty.COLUMN_ENCRYPTION.getDefaultValue();
-                activeConnectionProperties.setProperty(sPropKey, sPropValue);
-            }
-            columnEncryptionSetting = ColumnEncryptionSetting.valueOfString(sPropValue).toString();
+                String instanceValueFromProp = activeConnectionProperties.getProperty(instanceNameProperty);
+                // property takes precedence
+                if (null != instanceValueFromProp)
+                    instanceValue = instanceValueFromProp;
 
-            sPropKey = SQLServerDriverStringProperty.ENCLAVE_ATTESTATION_URL.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null != sPropValue) {
-                enclaveAttestationUrl = sPropValue;
-            }
+                if (instanceValue != null) {
+                    validateMaxSQLLoginName(instanceNameProperty, instanceValue);
+                    // only get port if the port is not specified
+                    activeConnectionProperties.setProperty(instanceNameProperty, instanceValue);
+                    trustedServerNameAE += "\\" + instanceValue;
+                }
 
-            sPropKey = SQLServerDriverStringProperty.ENCLAVE_ATTESTATION_PROTOCOL.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null != sPropValue) {
-                enclaveAttestationProtocol = sPropValue;
-                if (!AttestationProtocol.isValidAttestationProtocol(enclaveAttestationProtocol)) {
-                    throw new SQLServerException(SQLServerException.getErrString("R_enclaveInvalidAttestationProtocol"),
-                            null);
+                if (null != sPropValuePort) {
+                    trustedServerNameAE += ":" + sPropValuePort;
                 }
 
-                if (enclaveAttestationProtocol.equalsIgnoreCase(AttestationProtocol.HGS.toString())) {
-                    this.enclaveProvider = new SQLServerVSMEnclaveProvider();
-                } else {
-                    // If it's a valid Provider & not HGS, then it has to be AAS
-                    this.enclaveProvider = new SQLServerAASEnclaveProvider();
+                sPropKey = SQLServerDriverStringProperty.APPLICATION_NAME.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null != sPropValue)
+                    validateMaxSQLLoginName(sPropKey, sPropValue);
+                else
+                    activeConnectionProperties.setProperty(sPropKey, SQLServerDriver.DEFAULT_APP_NAME);
+
+                sPropKey = SQLServerDriverBooleanProperty.LAST_UPDATE_COUNT.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.LAST_UPDATE_COUNT.getDefaultValue());
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
                 }
-            }
 
-            // enclave requires columnEncryption=enabled, enclaveAttestationUrl and enclaveAttestationProtocol
-            if ((null != enclaveAttestationUrl && !enclaveAttestationUrl.isEmpty()
-                    && (null == enclaveAttestationProtocol || enclaveAttestationProtocol.isEmpty()))
-                    || (null != enclaveAttestationProtocol && !enclaveAttestationProtocol.isEmpty()
-                            && (null == enclaveAttestationUrl || enclaveAttestationUrl.isEmpty()))
-                    || (null != enclaveAttestationUrl && !enclaveAttestationUrl.isEmpty()
-                            && (null != enclaveAttestationProtocol || !enclaveAttestationProtocol.isEmpty())
-                            && (null == columnEncryptionSetting || !isColumnEncryptionSettingEnabled()))) {
-                throw new SQLServerException(SQLServerException.getErrString("R_enclavePropertiesError"), null);
-            }
+                sPropKey = SQLServerDriverStringProperty.COLUMN_ENCRYPTION.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = SQLServerDriverStringProperty.COLUMN_ENCRYPTION.getDefaultValue();
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                }
+                columnEncryptionSetting = ColumnEncryptionSetting.valueOfString(sPropValue).toString();
 
-            sPropKey = SQLServerDriverStringProperty.KEY_STORE_AUTHENTICATION.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null != sPropValue) {
-                keyStoreAuthentication = KeyStoreAuthentication.valueOfString(sPropValue).toString();
-            }
+                sPropKey = SQLServerDriverStringProperty.ENCLAVE_ATTESTATION_URL.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null != sPropValue) {
+                    enclaveAttestationUrl = sPropValue;
+                }
 
-            sPropKey = SQLServerDriverStringProperty.KEY_STORE_SECRET.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null != sPropValue) {
-                keyStoreSecret = sPropValue;
-            }
+                sPropKey = SQLServerDriverStringProperty.ENCLAVE_ATTESTATION_PROTOCOL.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null != sPropValue) {
+                    enclaveAttestationProtocol = sPropValue;
+                    if (!AttestationProtocol.isValidAttestationProtocol(enclaveAttestationProtocol)) {
+                        throw new SQLServerException(
+                                SQLServerException.getErrString("R_enclaveInvalidAttestationProtocol"), null);
+                    }
 
-            sPropKey = SQLServerDriverStringProperty.KEY_STORE_LOCATION.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null != sPropValue) {
-                keyStoreLocation = sPropValue;
-            }
+                    if (enclaveAttestationProtocol.equalsIgnoreCase(AttestationProtocol.HGS.toString())) {
+                        this.enclaveProvider = new SQLServerVSMEnclaveProvider();
+                    } else {
+                        // If it's a valid Provider & not HGS, then it has to be AAS
+                        this.enclaveProvider = new SQLServerAASEnclaveProvider();
+                    }
+                }
 
-            sPropKey = SQLServerDriverStringProperty.KEY_STORE_PRINCIPAL_ID.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null != sPropValue) {
-                keyStorePrincipalId = sPropValue;
-            }
+                // enclave requires columnEncryption=enabled, enclaveAttestationUrl and enclaveAttestationProtocol
+                if ((null != enclaveAttestationUrl && !enclaveAttestationUrl.isEmpty()
+                        && (null == enclaveAttestationProtocol || enclaveAttestationProtocol.isEmpty()))
+                        || (null != enclaveAttestationProtocol && !enclaveAttestationProtocol.isEmpty()
+                                && (null == enclaveAttestationUrl || enclaveAttestationUrl.isEmpty()))
+                        || (null != enclaveAttestationUrl && !enclaveAttestationUrl.isEmpty()
+                                && (null != enclaveAttestationProtocol || !enclaveAttestationProtocol.isEmpty())
+                                && (null == columnEncryptionSetting || !isColumnEncryptionSettingEnabled()))) {
+                    throw new SQLServerException(SQLServerException.getErrString("R_enclavePropertiesError"), null);
+                }
 
-            registerKeyStoreProviderOnConnection(keyStoreAuthentication, keyStoreSecret, keyStoreLocation);
+                sPropKey = SQLServerDriverStringProperty.KEY_STORE_AUTHENTICATION.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null != sPropValue) {
+                    keyStoreAuthentication = KeyStoreAuthentication.valueOfString(sPropValue).toString();
+                }
 
-            sPropKey = SQLServerDriverStringProperty.KEY_VAULT_PROVIDER_CLIENT_ID.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null != sPropValue) {
-                if (null != keyStoreAuthentication) {
-                    throw new SQLServerException(SQLServerException.getErrString("R_keyVaultProviderNotSupportedWithKeyStoreAuthentication"), null);
+                sPropKey = SQLServerDriverStringProperty.KEY_STORE_SECRET.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null != sPropValue) {
+                    keyStoreSecret = sPropValue;
                 }
-                String keyVaultColumnEncryptionProviderClientId = sPropValue;
-                sPropKey = SQLServerDriverStringProperty.KEY_VAULT_PROVIDER_CLIENT_KEY.toString();
+
+                sPropKey = SQLServerDriverStringProperty.KEY_STORE_LOCATION.toString();
                 sPropValue = activeConnectionProperties.getProperty(sPropKey);
-                if (null == sPropValue) {
-                    throw new SQLServerException(SQLServerException.getErrString("R_keyVaultProviderClientKeyNotSet"), null);
+                if (null != sPropValue) {
+                    keyStoreLocation = sPropValue;
                 }
-                String keyVaultColumnEncryptionProviderClientKey = sPropValue;
-                SQLServerColumnEncryptionAzureKeyVaultProvider provider = new SQLServerColumnEncryptionAzureKeyVaultProvider(
-                        keyVaultColumnEncryptionProviderClientId, keyVaultColumnEncryptionProviderClientKey);
-                systemColumnEncryptionKeyStoreProvider.put(provider.getName(), provider);
-            }
 
-            sPropKey = SQLServerDriverBooleanProperty.MULTI_SUBNET_FAILOVER.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null == sPropValue) {
-                sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.MULTI_SUBNET_FAILOVER.getDefaultValue());
-                activeConnectionProperties.setProperty(sPropKey, sPropValue);
-            }
-            multiSubnetFailover = isBooleanPropertyOn(sPropKey, sPropValue);
-
-            sPropKey = SQLServerDriverBooleanProperty.TRANSPARENT_NETWORK_IP_RESOLUTION.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null == sPropValue) {
-                userSetTNIR = false;
-                sPropValue = Boolean
-                        .toString(SQLServerDriverBooleanProperty.TRANSPARENT_NETWORK_IP_RESOLUTION.getDefaultValue());
-                activeConnectionProperties.setProperty(sPropKey, sPropValue);
-            }
-            transparentNetworkIPResolution = isBooleanPropertyOn(sPropKey, sPropValue);
+                sPropKey = SQLServerDriverStringProperty.KEY_STORE_PRINCIPAL_ID.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null != sPropValue) {
+                    keyStorePrincipalId = sPropValue;
+                }
 
-            sPropKey = SQLServerDriverBooleanProperty.ENCRYPT.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null == sPropValue) {
-                sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.ENCRYPT.getDefaultValue());
-                activeConnectionProperties.setProperty(sPropKey, sPropValue);
-            }
+                registerKeyStoreProviderOnConnection(keyStoreAuthentication, keyStoreSecret, keyStoreLocation);
 
-            socketFactoryClass = activeConnectionProperties
-                    .getProperty(SQLServerDriverStringProperty.SOCKET_FACTORY_CLASS.toString());
-            socketFactoryConstructorArg = activeConnectionProperties
-                    .getProperty(SQLServerDriverStringProperty.SOCKET_FACTORY_CONSTRUCTOR_ARG.toString());
+                sPropKey = SQLServerDriverStringProperty.KEY_VAULT_PROVIDER_CLIENT_ID.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null != sPropValue) {
+                    if (null != keyStoreAuthentication) {
+                        throw new SQLServerException(SQLServerException
+                                .getErrString("R_keyVaultProviderNotSupportedWithKeyStoreAuthentication"), null);
+                    }
+                    String keyVaultColumnEncryptionProviderClientId = sPropValue;
+                    sPropKey = SQLServerDriverStringProperty.KEY_VAULT_PROVIDER_CLIENT_KEY.toString();
+                    sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                    if (null == sPropValue) {
+                        throw new SQLServerException(
+                                SQLServerException.getErrString("R_keyVaultProviderClientKeyNotSet"), null);
+                    }
+                    String keyVaultColumnEncryptionProviderClientKey = sPropValue;
+                    SQLServerColumnEncryptionAzureKeyVaultProvider provider = new SQLServerColumnEncryptionAzureKeyVaultProvider(
+                            keyVaultColumnEncryptionProviderClientId, keyVaultColumnEncryptionProviderClientKey);
+                    systemColumnEncryptionKeyStoreProvider.put(provider.getName(), provider);
+                }
 
-            // Set requestedEncryptionLevel according to the value of the encrypt connection property
-            requestedEncryptionLevel = isBooleanPropertyOn(sPropKey, sPropValue) ? TDS.ENCRYPT_ON : TDS.ENCRYPT_OFF;
+                sPropKey = SQLServerDriverBooleanProperty.MULTI_SUBNET_FAILOVER.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = Boolean
+                            .toString(SQLServerDriverBooleanProperty.MULTI_SUBNET_FAILOVER.getDefaultValue());
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                }
+                multiSubnetFailover = isBooleanPropertyOn(sPropKey, sPropValue);
 
-            sPropKey = SQLServerDriverBooleanProperty.TRUST_SERVER_CERTIFICATE.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null == sPropValue) {
-                sPropValue = Boolean
-                        .toString(SQLServerDriverBooleanProperty.TRUST_SERVER_CERTIFICATE.getDefaultValue());
-                activeConnectionProperties.setProperty(sPropKey, sPropValue);
-            }
+                sPropKey = SQLServerDriverBooleanProperty.TRANSPARENT_NETWORK_IP_RESOLUTION.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    userSetTNIR = false;
+                    sPropValue = Boolean.toString(
+                            SQLServerDriverBooleanProperty.TRANSPARENT_NETWORK_IP_RESOLUTION.getDefaultValue());
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                }
+                transparentNetworkIPResolution = isBooleanPropertyOn(sPropKey, sPropValue);
 
-            trustServerCertificate = isBooleanPropertyOn(sPropKey, sPropValue);
+                sPropKey = SQLServerDriverBooleanProperty.ENCRYPT.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.ENCRYPT.getDefaultValue());
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                }
 
-            trustManagerClass = activeConnectionProperties
-                    .getProperty(SQLServerDriverStringProperty.TRUST_MANAGER_CLASS.toString());
-            trustManagerConstructorArg = activeConnectionProperties
-                    .getProperty(SQLServerDriverStringProperty.TRUST_MANAGER_CONSTRUCTOR_ARG.toString());
+                socketFactoryClass = activeConnectionProperties
+                        .getProperty(SQLServerDriverStringProperty.SOCKET_FACTORY_CLASS.toString());
+                socketFactoryConstructorArg = activeConnectionProperties
+                        .getProperty(SQLServerDriverStringProperty.SOCKET_FACTORY_CONSTRUCTOR_ARG.toString());
 
-            sPropKey = SQLServerDriverStringProperty.SELECT_METHOD.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null == sPropValue) {
-                sPropValue = SQLServerDriverStringProperty.SELECT_METHOD.getDefaultValue();
-            }
+                // Set requestedEncryptionLevel according to the value of the encrypt connection property
+                requestedEncryptionLevel = isBooleanPropertyOn(sPropKey, sPropValue) ? TDS.ENCRYPT_ON : TDS.ENCRYPT_OFF;
 
-            if ("cursor".equalsIgnoreCase(sPropValue) || "direct".equalsIgnoreCase(sPropValue)) {
-                sPropValue = sPropValue.toLowerCase(Locale.ENGLISH);
-                activeConnectionProperties.setProperty(sPropKey, sPropValue);
-                selectMethod = sPropValue;
-            } else {
-                MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidselectMethod"));
-                Object[] msgArgs = {sPropValue};
-                SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
-            }
+                sPropKey = SQLServerDriverBooleanProperty.TRUST_SERVER_CERTIFICATE.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = Boolean
+                            .toString(SQLServerDriverBooleanProperty.TRUST_SERVER_CERTIFICATE.getDefaultValue());
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                }
 
-            sPropKey = SQLServerDriverStringProperty.RESPONSE_BUFFERING.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null == sPropValue) {
-                sPropValue = SQLServerDriverStringProperty.RESPONSE_BUFFERING.getDefaultValue();
-            }
+                trustServerCertificate = isBooleanPropertyOn(sPropKey, sPropValue);
+                trustManagerClass = activeConnectionProperties
+                        .getProperty(SQLServerDriverStringProperty.TRUST_MANAGER_CLASS.toString());
+                trustManagerConstructorArg = activeConnectionProperties
+                        .getProperty(SQLServerDriverStringProperty.TRUST_MANAGER_CONSTRUCTOR_ARG.toString());
 
-            if ("full".equalsIgnoreCase(sPropValue) || "adaptive".equalsIgnoreCase(sPropValue)) {
-                activeConnectionProperties.setProperty(sPropKey, sPropValue.toLowerCase(Locale.ENGLISH));
-            } else {
-                MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidresponseBuffering"));
-                Object[] msgArgs = {sPropValue};
-                SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
-            }
+                sPropKey = SQLServerDriverStringProperty.SELECT_METHOD.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = SQLServerDriverStringProperty.SELECT_METHOD.getDefaultValue();
+                }
 
-            sPropKey = SQLServerDriverStringProperty.APPLICATION_INTENT.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null == sPropValue) {
-                sPropValue = SQLServerDriverStringProperty.APPLICATION_INTENT.getDefaultValue();
-            }
+                if ("cursor".equalsIgnoreCase(sPropValue) || "direct".equalsIgnoreCase(sPropValue)) {
+                    sPropValue = sPropValue.toLowerCase(Locale.ENGLISH);
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                    selectMethod = sPropValue;
+                } else {
+                    MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidselectMethod"));
+                    Object[] msgArgs = {sPropValue};
+                    SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
+                }
 
-            applicationIntent = ApplicationIntent.valueOfString(sPropValue);
-            activeConnectionProperties.setProperty(sPropKey, applicationIntent.toString());
+                sPropKey = SQLServerDriverStringProperty.RESPONSE_BUFFERING.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = SQLServerDriverStringProperty.RESPONSE_BUFFERING.getDefaultValue();
+                }
 
-            sPropKey = SQLServerDriverBooleanProperty.REPLICATION.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null == sPropValue) {
-                sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.REPLICATION.getDefaultValue());
-                activeConnectionProperties.setProperty(sPropKey, sPropValue);
-            }
+                if ("full".equalsIgnoreCase(sPropValue) || "adaptive".equalsIgnoreCase(sPropValue)) {
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue.toLowerCase(Locale.ENGLISH));
+                } else {
+                    MessageFormat form = new MessageFormat(
+                            SQLServerException.getErrString("R_invalidresponseBuffering"));
+                    Object[] msgArgs = {sPropValue};
+                    SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
+                }
 
-            replication = isBooleanPropertyOn(sPropKey, sPropValue);
+                sPropKey = SQLServerDriverStringProperty.APPLICATION_INTENT.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = SQLServerDriverStringProperty.APPLICATION_INTENT.getDefaultValue();
+                }
 
-            sPropKey = SQLServerDriverBooleanProperty.SEND_TIME_AS_DATETIME.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null == sPropValue) {
-                sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.SEND_TIME_AS_DATETIME.getDefaultValue());
-                activeConnectionProperties.setProperty(sPropKey, sPropValue);
-            }
+                applicationIntent = ApplicationIntent.valueOfString(sPropValue);
+                activeConnectionProperties.setProperty(sPropKey, applicationIntent.toString());
 
-            sendTimeAsDatetime = isBooleanPropertyOn(sPropKey, sPropValue);
+                sPropKey = SQLServerDriverBooleanProperty.REPLICATION.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.REPLICATION.getDefaultValue());
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                }
 
-            sPropKey = SQLServerDriverBooleanProperty.USE_FMT_ONLY.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null == sPropValue) {
-                sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.USE_FMT_ONLY.getDefaultValue());
-                activeConnectionProperties.setProperty(sPropKey, sPropValue);
-            }
-            useFmtOnly = isBooleanPropertyOn(sPropKey, sPropValue);
+                replication = isBooleanPropertyOn(sPropKey, sPropValue);
 
-            // Must be set before DISABLE_STATEMENT_POOLING
-            sPropKey = SQLServerDriverIntProperty.STATEMENT_POOLING_CACHE_SIZE.toString();
-            if (activeConnectionProperties.getProperty(sPropKey) != null
-                    && activeConnectionProperties.getProperty(sPropKey).length() > 0) {
-                try {
-                    int n = Integer.parseInt(activeConnectionProperties.getProperty(sPropKey));
-                    this.setStatementPoolingCacheSize(n);
-                } catch (NumberFormatException e) {
-                    MessageFormat form = new MessageFormat(
-                            SQLServerException.getErrString("R_statementPoolingCacheSize"));
-                    Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)};
-                    SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
+                sPropKey = SQLServerDriverBooleanProperty.SEND_TIME_AS_DATETIME.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = Boolean
+                            .toString(SQLServerDriverBooleanProperty.SEND_TIME_AS_DATETIME.getDefaultValue());
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
                 }
-            }
 
-            sPropKey = SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_ID.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null == sPropValue) {
-                sPropValue = SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_ID.getDefaultValue();
-                activeConnectionProperties.setProperty(sPropKey, sPropValue);
-            }
-            aadPrincipalID = sPropValue;
+                sendTimeAsDatetime = isBooleanPropertyOn(sPropKey, sPropValue);
 
-            sPropKey = SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_SECRET.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null == sPropValue) {
-                sPropValue = SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_SECRET.getDefaultValue();
-                activeConnectionProperties.setProperty(sPropKey, sPropValue);
-            }
-            aadPrincipalSecret = sPropValue;
+                sPropKey = SQLServerDriverBooleanProperty.USE_FMT_ONLY.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.USE_FMT_ONLY.getDefaultValue());
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                }
+                useFmtOnly = isBooleanPropertyOn(sPropKey, sPropValue);
 
-            // Must be set after STATEMENT_POOLING_CACHE_SIZE
-            sPropKey = SQLServerDriverBooleanProperty.DISABLE_STATEMENT_POOLING.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null != sPropValue) {
-                setDisableStatementPooling(isBooleanPropertyOn(sPropKey, sPropValue));
-            }
+                // Must be set before DISABLE_STATEMENT_POOLING
+                sPropKey = SQLServerDriverIntProperty.STATEMENT_POOLING_CACHE_SIZE.toString();
+                if (activeConnectionProperties.getProperty(sPropKey) != null
+                        && activeConnectionProperties.getProperty(sPropKey).length() > 0) {
+                    try {
+                        int n = Integer.parseInt(activeConnectionProperties.getProperty(sPropKey));
+                        this.setStatementPoolingCacheSize(n);
+                    } catch (NumberFormatException e) {
+                        MessageFormat form = new MessageFormat(
+                                SQLServerException.getErrString("R_statementPoolingCacheSize"));
+                        Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)};
+                        SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
+                    }
+                }
 
-            sPropKey = SQLServerDriverBooleanProperty.INTEGRATED_SECURITY.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null != sPropValue) {
-                integratedSecurity = isBooleanPropertyOn(sPropKey, sPropValue);
-            }
+                sPropKey = SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_ID.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_ID.getDefaultValue();
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                }
+                aadPrincipalID = sPropValue;
 
-            // Ignore authenticationScheme setting if integrated authentication not specified
-            if (integratedSecurity) {
-                sPropKey = SQLServerDriverStringProperty.AUTHENTICATION_SCHEME.toString();
+                sPropKey = SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_SECRET.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_SECRET.getDefaultValue();
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                }
+                aadPrincipalSecret = sPropValue;
+
+                // Must be set after STATEMENT_POOLING_CACHE_SIZE
+                sPropKey = SQLServerDriverBooleanProperty.DISABLE_STATEMENT_POOLING.toString();
                 sPropValue = activeConnectionProperties.getProperty(sPropKey);
                 if (null != sPropValue) {
-                    intAuthScheme = AuthenticationScheme.valueOfString(sPropValue);
+                    setDisableStatementPooling(isBooleanPropertyOn(sPropKey, sPropValue));
                 }
-            }
 
-            if (intAuthScheme == AuthenticationScheme.javaKerberos) {
-                sPropKey = SQLServerDriverObjectProperty.GSS_CREDENTIAL.toString();
-                if (activeConnectionProperties.containsKey(sPropKey)) {
-                    impersonatedUserCred = (GSSCredential) activeConnectionProperties.get(sPropKey);
-                    isUserCreatedCredential = true;
+                sPropKey = SQLServerDriverBooleanProperty.INTEGRATED_SECURITY.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null != sPropValue) {
+                    integratedSecurity = isBooleanPropertyOn(sPropKey, sPropValue);
                 }
-            } else if (intAuthScheme == AuthenticationScheme.ntlm) {
-                String sPropKeyDomain = SQLServerDriverStringProperty.DOMAIN.toString();
-                String sPropValueDomain = activeConnectionProperties.getProperty(sPropKeyDomain);
-                if (null == sPropValueDomain) {
-                    activeConnectionProperties.setProperty(sPropKeyDomain,
-                            SQLServerDriverStringProperty.DOMAIN.getDefaultValue());
+
+                // Ignore authenticationScheme setting if integrated authentication not specified
+                if (integratedSecurity) {
+                    sPropKey = SQLServerDriverStringProperty.AUTHENTICATION_SCHEME.toString();
+                    sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                    if (null != sPropValue) {
+                        intAuthScheme = AuthenticationScheme.valueOfString(sPropValue);
+                    }
                 }
 
-                // NTLM and no user or password
-                if (activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString()).isEmpty()
-                        || activeConnectionProperties.getProperty(SQLServerDriverStringProperty.PASSWORD.toString())
-                                .isEmpty()) {
+                if (intAuthScheme == AuthenticationScheme.javaKerberos) {
+                    sPropKey = SQLServerDriverObjectProperty.GSS_CREDENTIAL.toString();
+                    if (activeConnectionProperties.containsKey(sPropKey)) {
+                        impersonatedUserCred = (GSSCredential) activeConnectionProperties.get(sPropKey);
+                        isUserCreatedCredential = true;
+                    }
+                } else if (intAuthScheme == AuthenticationScheme.ntlm) {
+                    String sPropKeyDomain = SQLServerDriverStringProperty.DOMAIN.toString();
+                    String sPropValueDomain = activeConnectionProperties.getProperty(sPropKeyDomain);
+                    if (null == sPropValueDomain) {
+                        activeConnectionProperties.setProperty(sPropKeyDomain,
+                                SQLServerDriverStringProperty.DOMAIN.getDefaultValue());
+                    }
 
-                    if (connectionlogger.isLoggable(Level.SEVERE)) {
-                        connectionlogger.severe(
-                                toString() + " " + SQLServerException.getErrString("R_NtlmNoUserPasswordDomain"));
+                    // NTLM and no user or password
+                    if (activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString()).isEmpty()
+                            || activeConnectionProperties.getProperty(SQLServerDriverStringProperty.PASSWORD.toString())
+                                    .isEmpty()) {
+
+                        if (connectionlogger.isLoggable(Level.SEVERE)) {
+                            connectionlogger.severe(
+                                    toString() + " " + SQLServerException.getErrString("R_NtlmNoUserPasswordDomain"));
+                        }
+                        throw new SQLServerException(SQLServerException.getErrString("R_NtlmNoUserPasswordDomain"),
+                                null);
                     }
-                    throw new SQLServerException(SQLServerException.getErrString("R_NtlmNoUserPasswordDomain"), null);
+                    ntlmAuthentication = true;
                 }
-                ntlmAuthentication = true;
-            }
-
-            sPropKey = SQLServerDriverStringProperty.AUTHENTICATION.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null == sPropValue) {
-                sPropValue = SQLServerDriverStringProperty.AUTHENTICATION.getDefaultValue();
-            }
-            authenticationString = SqlAuthentication.valueOfString(sPropValue).toString().trim();
 
-            if (integratedSecurity
-                    && !authenticationString.equalsIgnoreCase(SqlAuthentication.NotSpecified.toString())) {
-                if (connectionlogger.isLoggable(Level.SEVERE)) {
-                    connectionlogger.severe(toString() + " "
-                            + SQLServerException.getErrString("R_SetAuthenticationWhenIntegratedSecurityTrue"));
+                sPropKey = SQLServerDriverStringProperty.AUTHENTICATION.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = SQLServerDriverStringProperty.AUTHENTICATION.getDefaultValue();
                 }
-                throw new SQLServerException(
-                        SQLServerException.getErrString("R_SetAuthenticationWhenIntegratedSecurityTrue"), null);
-            }
+                authenticationString = SqlAuthentication.valueOfString(sPropValue).toString().trim();
 
-            if (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString())
-                    && ((!activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString())
-                            .isEmpty())
-                            || (!activeConnectionProperties
-                                    .getProperty(SQLServerDriverStringProperty.PASSWORD.toString()).isEmpty()))) {
-                if (connectionlogger.isLoggable(Level.SEVERE)) {
-                    connectionlogger.severe(toString() + " "
-                            + SQLServerException.getErrString("R_IntegratedAuthenticationWithUserPassword"));
-                }
-                throw new SQLServerException(
-                        SQLServerException.getErrString("R_IntegratedAuthenticationWithUserPassword"), null);
-            }
+                if (integratedSecurity
+                        && !authenticationString.equalsIgnoreCase(SqlAuthentication.NotSpecified.toString())) {
+                    if (connectionlogger.isLoggable(Level.SEVERE)) {
+                        connectionlogger.severe(toString() + " "
+                                + SQLServerException.getErrString("R_SetAuthenticationWhenIntegratedSecurityTrue"));
+                    }
+                    throw new SQLServerException(
+                            SQLServerException.getErrString("R_SetAuthenticationWhenIntegratedSecurityTrue"), null);
+                }
 
-            if (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryPassword.toString())
-                    && ((activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString())
-                            .isEmpty())
-                            || (activeConnectionProperties
-                                    .getProperty(SQLServerDriverStringProperty.PASSWORD.toString()).isEmpty()))) {
-                if (connectionlogger.isLoggable(Level.SEVERE)) {
-                    connectionlogger.severe(
-                            toString() + " " + SQLServerException.getErrString("R_NoUserPasswordForActivePassword"));
-                }
-                throw new SQLServerException(SQLServerException.getErrString("R_NoUserPasswordForActivePassword"),
-                        null);
-            }
+                if (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString())
+                        && ((!activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString())
+                                .isEmpty())
+                                || (!activeConnectionProperties
+                                        .getProperty(SQLServerDriverStringProperty.PASSWORD.toString()).isEmpty()))) {
+                    if (connectionlogger.isLoggable(Level.SEVERE)) {
+                        connectionlogger.severe(toString() + " "
+                                + SQLServerException.getErrString("R_IntegratedAuthenticationWithUserPassword"));
+                    }
+                    throw new SQLServerException(
+                            SQLServerException.getErrString("R_IntegratedAuthenticationWithUserPassword"), null);
+                }
 
-            if (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryMSI.toString())
-                    && ((!activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString())
-                            .isEmpty())
-                            || (!activeConnectionProperties
-                                    .getProperty(SQLServerDriverStringProperty.PASSWORD.toString()).isEmpty()))) {
-                if (connectionlogger.isLoggable(Level.SEVERE)) {
-                    connectionlogger.severe(
-                            toString() + " " + SQLServerException.getErrString("R_MSIAuthenticationWithUserPassword"));
-                }
-                throw new SQLServerException(SQLServerException.getErrString("R_MSIAuthenticationWithUserPassword"),
-                        null);
-            }
+                if (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryPassword.toString())
+                        && ((activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString())
+                                .isEmpty())
+                                || (activeConnectionProperties
+                                        .getProperty(SQLServerDriverStringProperty.PASSWORD.toString()).isEmpty()))) {
+                    if (connectionlogger.isLoggable(Level.SEVERE)) {
+                        connectionlogger.severe(toString() + " "
+                                + SQLServerException.getErrString("R_NoUserPasswordForActivePassword"));
+                    }
+                    throw new SQLServerException(SQLServerException.getErrString("R_NoUserPasswordForActivePassword"),
+                            null);
+                }
 
-            if (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryServicePrincipal.toString())
-                    && ((activeConnectionProperties
-                            .getProperty(SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_ID.toString()).isEmpty())
-                            || (activeConnectionProperties
-                                    .getProperty(SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_SECRET.toString())
-                                    .isEmpty()))) {
-                if (connectionlogger.isLoggable(Level.SEVERE)) {
-                    connectionlogger.severe(toString() + " "
-                            + SQLServerException.getErrString("R_NoUserPasswordForActiveServicePrincipal"));
-                }
-                throw new SQLServerException(
-                        SQLServerException.getErrString("R_NoUserPasswordForActiveServicePrincipal"), null);
-            }
+                if (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryMSI.toString())
+                        && ((!activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString())
+                                .isEmpty())
+                                || (!activeConnectionProperties
+                                        .getProperty(SQLServerDriverStringProperty.PASSWORD.toString()).isEmpty()))) {
+                    if (connectionlogger.isLoggable(Level.SEVERE)) {
+                        connectionlogger.severe(toString() + " "
+                                + SQLServerException.getErrString("R_MSIAuthenticationWithUserPassword"));
+                    }
+                    throw new SQLServerException(SQLServerException.getErrString("R_MSIAuthenticationWithUserPassword"),
+                            null);
+                }
 
-            if (authenticationString.equalsIgnoreCase(SqlAuthentication.SqlPassword.toString())
-                    && ((activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString())
-                            .isEmpty())
-                            || (activeConnectionProperties
-                                    .getProperty(SQLServerDriverStringProperty.PASSWORD.toString()).isEmpty()))) {
-                if (connectionlogger.isLoggable(Level.SEVERE)) {
-                    connectionlogger.severe(
-                            toString() + " " + SQLServerException.getErrString("R_NoUserPasswordForSqlPassword"));
+                if (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryServicePrincipal.toString())
+                        && ((activeConnectionProperties
+                                .getProperty(SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_ID.toString())
+                                .isEmpty())
+                                || (activeConnectionProperties
+                                        .getProperty(
+                                                SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_SECRET.toString())
+                                        .isEmpty()))) {
+                    if (connectionlogger.isLoggable(Level.SEVERE)) {
+                        connectionlogger.severe(toString() + " "
+                                + SQLServerException.getErrString("R_NoUserPasswordForActiveServicePrincipal"));
+                    }
+                    throw new SQLServerException(
+                            SQLServerException.getErrString("R_NoUserPasswordForActiveServicePrincipal"), null);
                 }
-                throw new SQLServerException(SQLServerException.getErrString("R_NoUserPasswordForSqlPassword"), null);
-            }
 
-            sPropKey = SQLServerDriverStringProperty.ACCESS_TOKEN.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null != sPropValue) {
-                accessTokenInByte = sPropValue.getBytes(UTF_16LE);
-            }
+                if (authenticationString.equalsIgnoreCase(SqlAuthentication.SqlPassword.toString())
+                        && ((activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString())
+                                .isEmpty())
+                                || (activeConnectionProperties
+                                        .getProperty(SQLServerDriverStringProperty.PASSWORD.toString()).isEmpty()))) {
+                    if (connectionlogger.isLoggable(Level.SEVERE)) {
+                        connectionlogger.severe(
+                                toString() + " " + SQLServerException.getErrString("R_NoUserPasswordForSqlPassword"));
+                    }
 
-            if ((null != accessTokenInByte) && 0 == accessTokenInByte.length) {
-                if (connectionlogger.isLoggable(Level.SEVERE)) {
-                    connectionlogger
-                            .severe(toString() + " " + SQLServerException.getErrString("R_AccessTokenCannotBeEmpty"));
+                    throw new SQLServerException(SQLServerException.getErrString("R_NoUserPasswordForSqlPassword"),
+                            null);
                 }
-                throw new SQLServerException(SQLServerException.getErrString("R_AccessTokenCannotBeEmpty"), null);
-            }
 
-            if (integratedSecurity && (null != accessTokenInByte)) {
-                if (connectionlogger.isLoggable(Level.SEVERE)) {
-                    connectionlogger.severe(toString() + " "
-                            + SQLServerException.getErrString("R_SetAccesstokenWhenIntegratedSecurityTrue"));
+                sPropKey = SQLServerDriverStringProperty.ACCESS_TOKEN.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null != sPropValue) {
+                    accessTokenInByte = sPropValue.getBytes(UTF_16LE);
                 }
-                throw new SQLServerException(
-                        SQLServerException.getErrString("R_SetAccesstokenWhenIntegratedSecurityTrue"), null);
-            }
 
-            if ((!authenticationString.equalsIgnoreCase(SqlAuthentication.NotSpecified.toString()))
-                    && (null != accessTokenInByte)) {
-                if (connectionlogger.isLoggable(Level.SEVERE)) {
-                    connectionlogger.severe(toString() + " "
-                            + SQLServerException.getErrString("R_SetBothAuthenticationAndAccessToken"));
+                if ((null != accessTokenInByte) && 0 == accessTokenInByte.length) {
+                    if (connectionlogger.isLoggable(Level.SEVERE)) {
+                        connectionlogger.severe(
+                                toString() + " " + SQLServerException.getErrString("R_AccessTokenCannotBeEmpty"));
+                    }
+                    throw new SQLServerException(SQLServerException.getErrString("R_AccessTokenCannotBeEmpty"), null);
                 }
-                throw new SQLServerException(SQLServerException.getErrString("R_SetBothAuthenticationAndAccessToken"),
-                        null);
-            }
 
-            if ((null != accessTokenInByte) && ((!activeConnectionProperties
-                    .getProperty(SQLServerDriverStringProperty.USER.toString()).isEmpty())
-                    || (!activeConnectionProperties.getProperty(SQLServerDriverStringProperty.PASSWORD.toString())
-                            .isEmpty()))) {
-                if (connectionlogger.isLoggable(Level.SEVERE)) {
-                    connectionlogger.severe(
-                            toString() + " " + SQLServerException.getErrString("R_AccessTokenWithUserPassword"));
+                if (integratedSecurity && (null != accessTokenInByte)) {
+                    if (connectionlogger.isLoggable(Level.SEVERE)) {
+                        connectionlogger.severe(toString() + " "
+                                + SQLServerException.getErrString("R_SetAccesstokenWhenIntegratedSecurityTrue"));
+                    }
+                    throw new SQLServerException(
+                            SQLServerException.getErrString("R_SetAccesstokenWhenIntegratedSecurityTrue"), null);
                 }
-                throw new SQLServerException(SQLServerException.getErrString("R_AccessTokenWithUserPassword"), null);
-            }
 
-            // Turn off TNIR for FedAuth if user did not set TNIR explicitly
-            if (!userSetTNIR && (!authenticationString.equalsIgnoreCase(SqlAuthentication.NotSpecified.toString())
-                    || null != accessTokenInByte)) {
-                transparentNetworkIPResolution = false;
-            }
-
-            sPropKey = SQLServerDriverStringProperty.WORKSTATION_ID.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            validateMaxSQLLoginName(sPropKey, sPropValue);
-
-            int nPort = 0;
-            sPropKey = SQLServerDriverIntProperty.PORT_NUMBER.toString();
-            try {
-                String strPort = activeConnectionProperties.getProperty(sPropKey);
-                if (null != strPort) {
-                    nPort = Integer.parseInt(strPort);
+                if ((!authenticationString.equalsIgnoreCase(SqlAuthentication.NotSpecified.toString()))
+                        && (null != accessTokenInByte)) {
+                    if (connectionlogger.isLoggable(Level.SEVERE)) {
+                        connectionlogger.severe(toString() + " "
+                                + SQLServerException.getErrString("R_SetBothAuthenticationAndAccessToken"));
+                    }
+                    throw new SQLServerException(
+                            SQLServerException.getErrString("R_SetBothAuthenticationAndAccessToken"), null);
+                }
 
-                    if ((nPort < 0) || (nPort > 65535)) {
-                        MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidPortNumber"));
-                        Object[] msgArgs = {Integer.toString(nPort)};
-                        SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
+                if ((null != accessTokenInByte) && ((!activeConnectionProperties
+                        .getProperty(SQLServerDriverStringProperty.USER.toString()).isEmpty())
+                        || (!activeConnectionProperties.getProperty(SQLServerDriverStringProperty.PASSWORD.toString())
+                                .isEmpty()))) {
+                    if (connectionlogger.isLoggable(Level.SEVERE)) {
+                        connectionlogger.severe(
+                                toString() + " " + SQLServerException.getErrString("R_AccessTokenWithUserPassword"));
                     }
+                    throw new SQLServerException(SQLServerException.getErrString("R_AccessTokenWithUserPassword"),
+                            null);
                 }
-            } catch (NumberFormatException e) {
-                MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidPortNumber"));
-                Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)};
-                SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
-            }
 
-            // Handle optional packetSize property
-            sPropKey = SQLServerDriverIntProperty.PACKET_SIZE.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null != sPropValue && sPropValue.length() > 0) {
-                try {
-                    requestedPacketSize = Integer.parseInt(sPropValue);
+                // Turn off TNIR for FedAuth if user did not set TNIR explicitly
+                if (!userSetTNIR && (!authenticationString.equalsIgnoreCase(SqlAuthentication.NotSpecified.toString())
+                        || null != accessTokenInByte)) {
+                    transparentNetworkIPResolution = false;
+                }
 
-                    // -1 --> Use server default
-                    if (-1 == requestedPacketSize)
-                        requestedPacketSize = TDS.SERVER_PACKET_SIZE;
+                sPropKey = SQLServerDriverStringProperty.WORKSTATION_ID.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                validateMaxSQLLoginName(sPropKey, sPropValue);
 
-                    // 0 --> Use maximum size
-                    else if (0 == requestedPacketSize)
-                        requestedPacketSize = TDS.MAX_PACKET_SIZE;
+                int nPort = 0;
+                sPropKey = SQLServerDriverIntProperty.PORT_NUMBER.toString();
+                try {
+                    String strPort = activeConnectionProperties.getProperty(sPropKey);
+                    if (null != strPort) {
+                        nPort = Integer.parseInt(strPort);
+                        if ((nPort < 0) || (nPort > 65535)) {
+                            MessageFormat form = new MessageFormat(
+                                    SQLServerException.getErrString("R_invalidPortNumber"));
+                            Object[] msgArgs = {Integer.toString(nPort)};
+                            SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
+                        }
+                    }
                 } catch (NumberFormatException e) {
-                    // Ensure that an invalid prop value results in an invalid packet size that
-                    // is not acceptable to the server.
-                    requestedPacketSize = TDS.INVALID_PACKET_SIZE;
+                    MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidPortNumber"));
+                    Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)};
+                    SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
                 }
 
-                if (TDS.SERVER_PACKET_SIZE != requestedPacketSize) {
-                    // Complain if the packet size is not in the range acceptable to the server.
-                    if (requestedPacketSize < TDS.MIN_PACKET_SIZE || requestedPacketSize > TDS.MAX_PACKET_SIZE) {
-                        MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidPacketSize"));
-                        Object[] msgArgs = {sPropValue};
-                        SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
+                // Handle optional packetSize property
+                sPropKey = SQLServerDriverIntProperty.PACKET_SIZE.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null != sPropValue && sPropValue.length() > 0) {
+                    try {
+                        requestedPacketSize = Integer.parseInt(sPropValue);
+
+                        // -1 --> Use server default
+                        if (-1 == requestedPacketSize)
+                            requestedPacketSize = TDS.SERVER_PACKET_SIZE;
+
+                        // 0 --> Use maximum size
+                        else if (0 == requestedPacketSize)
+                            requestedPacketSize = TDS.MAX_PACKET_SIZE;
+                    } catch (NumberFormatException e) {
+                        // Ensure that an invalid prop value results in an invalid packet size that
+                        // is not acceptable to the server.
+                        requestedPacketSize = TDS.INVALID_PACKET_SIZE;
+                    }
+
+                    if (TDS.SERVER_PACKET_SIZE != requestedPacketSize) {
+                        // Complain if the packet size is not in the range acceptable to the server.
+                        if (requestedPacketSize < TDS.MIN_PACKET_SIZE || requestedPacketSize > TDS.MAX_PACKET_SIZE) {
+                            MessageFormat form = new MessageFormat(
+                                    SQLServerException.getErrString("R_invalidPacketSize"));
+                            Object[] msgArgs = {sPropValue};
+                            SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
+                        }
                     }
                 }
-            }
 
-            // Note isBooleanPropertyOn will throw exception if parsed value is not valid.
-
-            // have to check for null before calling isBooleanPropertyOn, because isBooleanPropertyOn
-            // assumes that the null property defaults to false.
-            sPropKey = SQLServerDriverBooleanProperty.SEND_STRING_PARAMETERS_AS_UNICODE.toString();
-            sendStringParametersAsUnicode = (null == activeConnectionProperties.getProperty(
-                    sPropKey)) ? SQLServerDriverBooleanProperty.SEND_STRING_PARAMETERS_AS_UNICODE.getDefaultValue()
-                               : isBooleanPropertyOn(sPropKey, activeConnectionProperties.getProperty(sPropKey));
-
-            sPropKey = SQLServerDriverBooleanProperty.LAST_UPDATE_COUNT.toString();
-            lastUpdateCount = isBooleanPropertyOn(sPropKey, activeConnectionProperties.getProperty(sPropKey));
-            sPropKey = SQLServerDriverBooleanProperty.XOPEN_STATES.toString();
-            xopenStates = isBooleanPropertyOn(sPropKey, activeConnectionProperties.getProperty(sPropKey));
-
-            sPropKey = SQLServerDriverStringProperty.RESPONSE_BUFFERING.toString();
-            responseBuffering = (null != activeConnectionProperties.getProperty(sPropKey)
-                    && activeConnectionProperties.getProperty(sPropKey).length() > 0)
-                                                                                      ? activeConnectionProperties
-                                                                                              .getProperty(sPropKey)
-                                                                                      : null;
-
-            sPropKey = SQLServerDriverIntProperty.LOCK_TIMEOUT.toString();
-            int defaultLockTimeOut = SQLServerDriverIntProperty.LOCK_TIMEOUT.getDefaultValue();
-            nLockTimeout = defaultLockTimeOut; // Wait forever
-            if (activeConnectionProperties.getProperty(sPropKey) != null
-                    && activeConnectionProperties.getProperty(sPropKey).length() > 0) {
-                try {
-                    int n = Integer.parseInt(activeConnectionProperties.getProperty(sPropKey));
-                    if (n >= defaultLockTimeOut)
-                        nLockTimeout = n;
-                    else {
+                // Note isBooleanPropertyOn will throw exception if parsed value is not valid.
+
+                // have to check for null before calling isBooleanPropertyOn, because isBooleanPropertyOn
+                // assumes that the null property defaults to false.
+                sPropKey = SQLServerDriverBooleanProperty.SEND_STRING_PARAMETERS_AS_UNICODE.toString();
+                sendStringParametersAsUnicode = (null == activeConnectionProperties.getProperty(
+                        sPropKey)) ? SQLServerDriverBooleanProperty.SEND_STRING_PARAMETERS_AS_UNICODE.getDefaultValue()
+                                   : isBooleanPropertyOn(sPropKey, activeConnectionProperties.getProperty(sPropKey));
+
+                sPropKey = SQLServerDriverBooleanProperty.LAST_UPDATE_COUNT.toString();
+                lastUpdateCount = isBooleanPropertyOn(sPropKey, activeConnectionProperties.getProperty(sPropKey));
+                sPropKey = SQLServerDriverBooleanProperty.XOPEN_STATES.toString();
+                xopenStates = isBooleanPropertyOn(sPropKey, activeConnectionProperties.getProperty(sPropKey));
+
+                sPropKey = SQLServerDriverStringProperty.RESPONSE_BUFFERING.toString();
+                responseBuffering = (null != activeConnectionProperties.getProperty(sPropKey)
+                        && activeConnectionProperties.getProperty(sPropKey).length() > 0)
+                                                                                          ? activeConnectionProperties
+                                                                                                  .getProperty(sPropKey)
+                                                                                          : null;
+
+                sPropKey = SQLServerDriverIntProperty.LOCK_TIMEOUT.toString();
+                int defaultLockTimeOut = SQLServerDriverIntProperty.LOCK_TIMEOUT.getDefaultValue();
+                nLockTimeout = defaultLockTimeOut; // Wait forever
+                if (activeConnectionProperties.getProperty(sPropKey) != null
+                        && activeConnectionProperties.getProperty(sPropKey).length() > 0) {
+                    try {
+                        int n = Integer.parseInt(activeConnectionProperties.getProperty(sPropKey));
+                        if (n >= defaultLockTimeOut)
+                            nLockTimeout = n;
+                        else {
+                            MessageFormat form = new MessageFormat(
+                                    SQLServerException.getErrString("R_invalidLockTimeOut"));
+                            Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)};
+                            SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
+                        }
+                    } catch (NumberFormatException e) {
                         MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidLockTimeOut"));
                         Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)};
                         SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
                     }
-                } catch (NumberFormatException e) {
-                    MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidLockTimeOut"));
-                    Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)};
-                    SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
                 }
-            }
 
-            sPropKey = SQLServerDriverIntProperty.QUERY_TIMEOUT.toString();
-            int defaultQueryTimeout = SQLServerDriverIntProperty.QUERY_TIMEOUT.getDefaultValue();
-            queryTimeoutSeconds = defaultQueryTimeout; // Wait forever
-            if (activeConnectionProperties.getProperty(sPropKey) != null
-                    && activeConnectionProperties.getProperty(sPropKey).length() > 0) {
-                try {
-                    int n = Integer.parseInt(activeConnectionProperties.getProperty(sPropKey));
-                    if (n >= defaultQueryTimeout) {
-                        queryTimeoutSeconds = n;
-                    } else {
+                sPropKey = SQLServerDriverIntProperty.QUERY_TIMEOUT.toString();
+                int defaultQueryTimeout = SQLServerDriverIntProperty.QUERY_TIMEOUT.getDefaultValue();
+                queryTimeoutSeconds = defaultQueryTimeout; // Wait forever
+                if (activeConnectionProperties.getProperty(sPropKey) != null
+                        && activeConnectionProperties.getProperty(sPropKey).length() > 0) {
+                    try {
+                        int n = Integer.parseInt(activeConnectionProperties.getProperty(sPropKey));
+                        if (n >= defaultQueryTimeout) {
+                            queryTimeoutSeconds = n;
+                        } else {
+                            MessageFormat form = new MessageFormat(
+                                    SQLServerException.getErrString("R_invalidQueryTimeout"));
+                            Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)};
+                            SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
+                        }
+                    } catch (NumberFormatException e) {
                         MessageFormat form = new MessageFormat(
                                 SQLServerException.getErrString("R_invalidQueryTimeout"));
                         Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)};
                         SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
                     }
-                } catch (NumberFormatException e) {
-                    MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidQueryTimeout"));
-                    Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)};
-                    SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
                 }
-            }
 
-            sPropKey = SQLServerDriverIntProperty.SOCKET_TIMEOUT.toString();
-            int defaultSocketTimeout = SQLServerDriverIntProperty.SOCKET_TIMEOUT.getDefaultValue();
-            socketTimeoutMilliseconds = defaultSocketTimeout; // Wait forever
-            if (activeConnectionProperties.getProperty(sPropKey) != null
-                    && activeConnectionProperties.getProperty(sPropKey).length() > 0) {
-                try {
-                    int n = Integer.parseInt(activeConnectionProperties.getProperty(sPropKey));
-                    if (n >= defaultSocketTimeout) {
-                        socketTimeoutMilliseconds = n;
-                    } else {
+                sPropKey = SQLServerDriverIntProperty.SOCKET_TIMEOUT.toString();
+                int defaultSocketTimeout = SQLServerDriverIntProperty.SOCKET_TIMEOUT.getDefaultValue();
+                socketTimeoutMilliseconds = defaultSocketTimeout; // Wait forever
+                if (activeConnectionProperties.getProperty(sPropKey) != null
+                        && activeConnectionProperties.getProperty(sPropKey).length() > 0) {
+                    try {
+                        int n = Integer.parseInt(activeConnectionProperties.getProperty(sPropKey));
+                        if (n >= defaultSocketTimeout) {
+                            socketTimeoutMilliseconds = n;
+                        } else {
+                            MessageFormat form = new MessageFormat(
+                                    SQLServerException.getErrString("R_invalidSocketTimeout"));
+                            Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)};
+                            SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
+                        }
+                    } catch (NumberFormatException e) {
+                        MessageFormat form = new MessageFormat(
+                                SQLServerException.getErrString("R_invalidSocketTimeout"));
+                        Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)};
+                        SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
+                    }
+                }
+
+                sPropKey = SQLServerDriverIntProperty.CANCEL_QUERY_TIMEOUT.toString();
+                int cancelQueryTimeout = SQLServerDriverIntProperty.CANCEL_QUERY_TIMEOUT.getDefaultValue();
+
+                if (activeConnectionProperties.getProperty(sPropKey) != null
+                        && activeConnectionProperties.getProperty(sPropKey).length() > 0) {
+                    try {
+                        int n = Integer.parseInt(activeConnectionProperties.getProperty(sPropKey));
+                        if (n >= cancelQueryTimeout) {
+                            // use cancelQueryTimeout only if queryTimeout is set.
+                            if (queryTimeoutSeconds > defaultQueryTimeout) {
+                                cancelQueryTimeoutSeconds = n;
+                            }
+                        } else {
+                            MessageFormat form = new MessageFormat(
+                                    SQLServerException.getErrString("R_invalidCancelQueryTimeout"));
+                            Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)};
+                            SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
+                        }
+                    } catch (NumberFormatException e) {
                         MessageFormat form = new MessageFormat(
-                                SQLServerException.getErrString("R_invalidSocketTimeout"));
+                                SQLServerException.getErrString("R_invalidCancelQueryTimeout"));
                         Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)};
                         SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
                     }
-                } catch (NumberFormatException e) {
-                    MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidSocketTimeout"));
-                    Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)};
-                    SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
                 }
-            }
 
-            sPropKey = SQLServerDriverIntProperty.CANCEL_QUERY_TIMEOUT.toString();
-            int cancelQueryTimeout = SQLServerDriverIntProperty.CANCEL_QUERY_TIMEOUT.getDefaultValue();
-
-            if (activeConnectionProperties.getProperty(sPropKey) != null
-                    && activeConnectionProperties.getProperty(sPropKey).length() > 0) {
-                try {
-                    int n = Integer.parseInt(activeConnectionProperties.getProperty(sPropKey));
-                    if (n >= cancelQueryTimeout) {
-                        // use cancelQueryTimeout only if queryTimeout is set.
-                        if (queryTimeoutSeconds > defaultQueryTimeout) {
-                            cancelQueryTimeoutSeconds = n;
-                        }
-                    } else {
+                sPropKey = SQLServerDriverIntProperty.SERVER_PREPARED_STATEMENT_DISCARD_THRESHOLD.toString();
+                if (activeConnectionProperties.getProperty(sPropKey) != null
+                        && activeConnectionProperties.getProperty(sPropKey).length() > 0) {
+                    try {
+                        int n = Integer.parseInt(activeConnectionProperties.getProperty(sPropKey));
+                        setServerPreparedStatementDiscardThreshold(n);
+                    } catch (NumberFormatException e) {
                         MessageFormat form = new MessageFormat(
-                                SQLServerException.getErrString("R_invalidCancelQueryTimeout"));
+                                SQLServerException.getErrString("R_serverPreparedStatementDiscardThreshold"));
                         Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)};
                         SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
                     }
-                } catch (NumberFormatException e) {
-                    MessageFormat form = new MessageFormat(
-                            SQLServerException.getErrString("R_invalidCancelQueryTimeout"));
-                    Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)};
-                    SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
                 }
-            }
 
-            sPropKey = SQLServerDriverIntProperty.SERVER_PREPARED_STATEMENT_DISCARD_THRESHOLD.toString();
-            if (activeConnectionProperties.getProperty(sPropKey) != null
-                    && activeConnectionProperties.getProperty(sPropKey).length() > 0) {
-                try {
-                    int n = Integer.parseInt(activeConnectionProperties.getProperty(sPropKey));
-                    setServerPreparedStatementDiscardThreshold(n);
-                } catch (NumberFormatException e) {
-                    MessageFormat form = new MessageFormat(
-                            SQLServerException.getErrString("R_serverPreparedStatementDiscardThreshold"));
-                    Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)};
-                    SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
+                sPropKey = SQLServerDriverBooleanProperty.ENABLE_PREPARE_ON_FIRST_PREPARED_STATEMENT.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null != sPropValue) {
+                    setEnablePrepareOnFirstPreparedStatementCall(isBooleanPropertyOn(sPropKey, sPropValue));
                 }
-            }
 
-            sPropKey = SQLServerDriverBooleanProperty.ENABLE_PREPARE_ON_FIRST_PREPARED_STATEMENT.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null != sPropValue) {
-                setEnablePrepareOnFirstPreparedStatementCall(isBooleanPropertyOn(sPropKey, sPropValue));
-            }
+                sPropKey = SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null != sPropValue) {
+                    useBulkCopyForBatchInsert = isBooleanPropertyOn(sPropKey, sPropValue);
+                }
 
-            sPropKey = SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null != sPropValue) {
-                useBulkCopyForBatchInsert = isBooleanPropertyOn(sPropKey, sPropValue);
-            }
+                sPropKey = SQLServerDriverStringProperty.SSL_PROTOCOL.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = SQLServerDriverStringProperty.SSL_PROTOCOL.getDefaultValue();
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                } else {
+                    activeConnectionProperties.setProperty(sPropKey, SSLProtocol.valueOfString(sPropValue).toString());
+                }
 
-            sPropKey = SQLServerDriverStringProperty.SSL_PROTOCOL.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null == sPropValue) {
-                sPropValue = SQLServerDriverStringProperty.SSL_PROTOCOL.getDefaultValue();
-                activeConnectionProperties.setProperty(sPropKey, sPropValue);
-            } else {
-                activeConnectionProperties.setProperty(sPropKey, SSLProtocol.valueOfString(sPropValue).toString());
-            }
+                sPropKey = SQLServerDriverStringProperty.MSI_CLIENT_ID.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null != sPropValue) {
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                }
 
-            sPropKey = SQLServerDriverStringProperty.MSI_CLIENT_ID.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null != sPropValue) {
-                activeConnectionProperties.setProperty(sPropKey, sPropValue);
-            }
+                sPropKey = SQLServerDriverStringProperty.CLIENT_CERTIFICATE.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null != sPropValue) {
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                    clientCertificate = sPropValue;
+                }
 
-            sPropKey = SQLServerDriverStringProperty.CLIENT_CERTIFICATE.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null != sPropValue) {
-                activeConnectionProperties.setProperty(sPropKey, sPropValue);
-                clientCertificate = sPropValue;
-            }
+                sPropKey = SQLServerDriverStringProperty.CLIENT_KEY.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null != sPropValue) {
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                    clientKey = sPropValue;
+                }
 
-            sPropKey = SQLServerDriverStringProperty.CLIENT_KEY.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null != sPropValue) {
-                activeConnectionProperties.setProperty(sPropKey, sPropValue);
-                clientKey = sPropValue;
-            }
+                sPropKey = SQLServerDriverStringProperty.CLIENT_KEY_PASSWORD.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null != sPropValue) {
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                    clientKeyPassword = sPropValue;
+                }
 
-            sPropKey = SQLServerDriverStringProperty.CLIENT_KEY_PASSWORD.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null != sPropValue) {
-                activeConnectionProperties.setProperty(sPropKey, sPropValue);
-                clientKeyPassword = sPropValue;
-            }
+                sPropKey = SQLServerDriverBooleanProperty.SEND_TEMPORAL_DATATYPES_AS_STRING_FOR_BULK_COPY.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null != sPropValue) {
+                    sendTemporalDataTypesAsStringForBulkCopy = isBooleanPropertyOn(sPropKey, sPropValue);
+                }
 
-            sPropKey = SQLServerDriverBooleanProperty.SEND_TEMPORAL_DATATYPES_AS_STRING_FOR_BULK_COPY.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null != sPropValue) {
-                sendTemporalDataTypesAsStringForBulkCopy = isBooleanPropertyOn(sPropKey, sPropValue);
-            }
+                sPropKey = SQLServerDriverStringProperty.MAX_RESULT_BUFFER.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                activeConnectionProperties.setProperty(sPropKey,
+                        String.valueOf(MaxResultBufferParser.validateMaxResultBuffer(sPropValue)));
 
-            sPropKey = SQLServerDriverStringProperty.MAX_RESULT_BUFFER.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            activeConnectionProperties.setProperty(sPropKey,
-                    String.valueOf(MaxResultBufferParser.validateMaxResultBuffer(sPropValue)));
+                sPropKey = SQLServerDriverBooleanProperty.DELAY_LOADING_LOBS.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.DELAY_LOADING_LOBS.getDefaultValue());
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                }
+                delayLoadingLobs = isBooleanPropertyOn(sPropKey, sPropValue);
 
-            sPropKey = SQLServerDriverBooleanProperty.DELAY_LOADING_LOBS.toString();
-            sPropValue = activeConnectionProperties.getProperty(sPropKey);
-            if (null == sPropValue) {
-                sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.DELAY_LOADING_LOBS.getDefaultValue());
-                activeConnectionProperties.setProperty(sPropKey, sPropValue);
-            }
-            delayLoadingLobs = isBooleanPropertyOn(sPropKey, sPropValue);
-
-            FailoverInfo fo = null;
-            String databaseNameProperty = SQLServerDriverStringProperty.DATABASE_NAME.toString();
-            String serverNameProperty = SQLServerDriverStringProperty.SERVER_NAME.toString();
-            String failOverPartnerProperty = SQLServerDriverStringProperty.FAILOVER_PARTNER.toString();
-            String failOverPartnerPropertyValue = activeConnectionProperties.getProperty(failOverPartnerProperty);
-
-            // failoverPartner and multiSubnetFailover=true cannot be used together
-            if (multiSubnetFailover && failOverPartnerPropertyValue != null) {
-                SQLServerException.makeFromDriverError(this, this,
-                        SQLServerException.getErrString("R_dbMirroringWithMultiSubnetFailover"), null, false);
-            }
+                FailoverInfo fo = null;
+                String databaseNameProperty = SQLServerDriverStringProperty.DATABASE_NAME.toString();
+                String serverNameProperty = SQLServerDriverStringProperty.SERVER_NAME.toString();
+                String failOverPartnerProperty = SQLServerDriverStringProperty.FAILOVER_PARTNER.toString();
+                String failOverPartnerPropertyValue = activeConnectionProperties.getProperty(failOverPartnerProperty);
 
-            // transparentNetworkIPResolution is ignored if multiSubnetFailover or DBMirroring is true and user did not
-            // set TNIR explicitly
-            if ((multiSubnetFailover || null != failOverPartnerPropertyValue) && !userSetTNIR) {
-                transparentNetworkIPResolution = false;
-            }
+                // failoverPartner and multiSubnetFailover=true cannot be used together
+                if (multiSubnetFailover && failOverPartnerPropertyValue != null) {
+                    SQLServerException.makeFromDriverError(this, this,
+                            SQLServerException.getErrString("R_dbMirroringWithMultiSubnetFailover"), null, false);
+                }
 
-            // failoverPartner and applicationIntent=ReadOnly cannot be used together
-            if ((applicationIntent != null) && applicationIntent.equals(ApplicationIntent.READ_ONLY)
-                    && failOverPartnerPropertyValue != null) {
-                SQLServerException.makeFromDriverError(this, this,
-                        SQLServerException.getErrString("R_dbMirroringWithReadOnlyIntent"), null, false);
-            }
+                // transparentNetworkIPResolution is ignored if multiSubnetFailover or DBMirroring is true and user did
+                // not set TNIR explicitly
+                if ((multiSubnetFailover || null != failOverPartnerPropertyValue) && !userSetTNIR) {
+                    transparentNetworkIPResolution = false;
+                }
 
-            // check to see failover specified without DB error here if not.
-            if (null != activeConnectionProperties.getProperty(databaseNameProperty)) {
-                // look to see if there exists a failover
-                fo = FailoverMapSingleton.getFailoverInfo(this,
-                        activeConnectionProperties.getProperty(serverNameProperty),
-                        activeConnectionProperties.getProperty(instanceNameProperty),
-                        activeConnectionProperties.getProperty(databaseNameProperty));
-            } else {
-                // it is an error to specify failover without db.
-                if (null != failOverPartnerPropertyValue)
+                // failoverPartner and applicationIntent=ReadOnly cannot be used together
+                if ((applicationIntent != null) && applicationIntent.equals(ApplicationIntent.READ_ONLY)
+                        && failOverPartnerPropertyValue != null) {
                     SQLServerException.makeFromDriverError(this, this,
-                            SQLServerException.getErrString("R_failoverPartnerWithoutDB"), null, true);
-            }
+                            SQLServerException.getErrString("R_dbMirroringWithReadOnlyIntent"), null, false);
+                }
+
+                // check to see failover specified without DB error here if not.
+                if (null != activeConnectionProperties.getProperty(databaseNameProperty)) {
+                    // look to see if there exists a failover
+                    fo = FailoverMapSingleton.getFailoverInfo(this,
+                            activeConnectionProperties.getProperty(serverNameProperty),
+                            activeConnectionProperties.getProperty(instanceNameProperty),
+                            activeConnectionProperties.getProperty(databaseNameProperty));
+                } else {
+                    // it is an error to specify failover without db.
+                    if (null != failOverPartnerPropertyValue)
+                        SQLServerException.makeFromDriverError(this, this,
+                                SQLServerException.getErrString("R_failoverPartnerWithoutDB"), null, true);
+                }
+
+                String mirror = (null == fo) ? failOverPartnerPropertyValue : null;
+
+                connectRetryCount = SQLServerDriverIntProperty.CONNECT_RETRY_COUNT.getDefaultValue();
+                sPropValue = activeConnectionProperties
+                        .getProperty(SQLServerDriverIntProperty.CONNECT_RETRY_COUNT.toString());
+                if (null != sPropValue && sPropValue.length() > 0) {
+                    try {
+                        connectRetryCount = Integer.parseInt(sPropValue);
+                    } catch (NumberFormatException e) {
+                        MessageFormat form = new MessageFormat(
+                                SQLServerException.getErrString("R_invalidConnectRetryCount"));
+                        Object[] msgArgs = {sPropValue};
+                        SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
+                    }
+                    if (connectRetryCount < 0 || connectRetryCount > 255) {
+                        MessageFormat form = new MessageFormat(
+                                SQLServerException.getErrString("R_invalidConnectRetryCount"));
+                        Object[] msgArgs = {sPropValue};
+                        SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
+                    }
+                }
 
-            String mirror = (null == fo) ? failOverPartnerPropertyValue : null;
+                connectRetryInterval = SQLServerDriverIntProperty.CONNECT_RETRY_INTERVAL.getDefaultValue();
+                sPropValue = activeConnectionProperties
+                        .getProperty(SQLServerDriverIntProperty.CONNECT_RETRY_INTERVAL.toString());
+                if (null != sPropValue && sPropValue.length() > 0) {
+                    try {
+                        connectRetryInterval = Integer.parseInt(sPropValue);
+                    } catch (NumberFormatException e) {
+                        MessageFormat form = new MessageFormat(
+                                SQLServerException.getErrString("R_invalidConnectRetryInterval"));
+                        Object[] msgArgs = {sPropValue};
+                        SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
+                    }
+
+                    if (connectRetryInterval < 1 || connectRetryInterval > 60) {
+                        MessageFormat form = new MessageFormat(
+                                SQLServerException.getErrString("R_invalidConnectRetryInterval"));
+                        Object[] msgArgs = {sPropValue};
+                        SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false);
+                    }
+                }
 
-            long startTime = System.currentTimeMillis();
-            login(activeConnectionProperties.getProperty(serverNameProperty), instanceValue, nPort, mirror, fo,
-                    loginTimeoutSeconds, startTime);
+                long startTime = System.currentTimeMillis();
+                sessionRecovery.setLoginParameters(instanceValue, nPort, fo,
+                        ((loginTimeoutSeconds > queryTimeoutSeconds) && queryTimeoutSeconds > 0) ? queryTimeoutSeconds
+                                                                                                 : loginTimeoutSeconds);
+                login(activeConnectionProperties.getProperty(serverNameProperty), instanceValue, nPort, mirror, fo,
+                        loginTimeoutSeconds, startTime);
+            } else {
+                long startTime = System.currentTimeMillis();
+                login(activeConnectionProperties.getProperty(SQLServerDriverStringProperty.SERVER_NAME.toString()),
+                        sessionRecovery.getInstanceValue(), sessionRecovery.getNPort(),
+                        activeConnectionProperties
+                                .getProperty(SQLServerDriverStringProperty.FAILOVER_PARTNER.toString()),
+                        sessionRecovery.getFailoverInfo(), sessionRecovery.getLoginTimeoutSeconds(), startTime);
+            }
 
             // If SSL is to be used for the duration of the connection, then make sure
             // that the final negotiated TDS packet size is no larger than the SSL record size.
@@ -2599,7 +2728,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                 if (!state.equals(State.Closed))
                     this.close();
             }
-            
+
             activeConnectionProperties.remove(SQLServerDriverStringProperty.TRUST_STORE_PASSWORD.toString());
         }
 
@@ -2616,6 +2745,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
             FailoverInfo foActual, int timeout, long timerStart) throws SQLServerException {
         // standardLogin would be false only for db mirroring scenarios. It would be true
         // for all other cases, including multiSubnetFailover
+
         final boolean isDBMirroring = null != mirror || null != foActual;
 
         int sleepInterval = 100; // milliseconds to sleep (back off) between attempts.
@@ -2801,8 +2931,8 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                                                                                                  // packetsize, etc)
                         || SQLServerException.ERROR_SOCKET_TIMEOUT == driverErrorCode // socket timeout
                         || (timerHasExpired(timerExpire) && !isInteractive) // no time to try again and not interactive
-                        // for non-dbmirroring cases, do not retry after tcp socket connection succeeds
-                        || (state.equals(State.Connected) && !isDBMirroring)) {
+                // for non-dbmirroring cases, do not retry after tcp socket connection succeeds
+                ) {
                     // close the connection and throw the error back
                     close();
                     throw sqlex;
@@ -2942,6 +3072,33 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         }
     }
 
+    boolean isFatalError(SQLServerException e) {
+        /*
+         * NOTE: If these conditions are modified, consider modification to conditions in SQLServerConnection::login()
+         * and Reconnect::run()
+         */
+
+        // actual logon failed (e.g. bad password)
+        if ((SQLServerException.LOGON_FAILED == e.getErrorCode())
+                // actual logon failed (e.g. password expired)
+                || (SQLServerException.PASSWORD_EXPIRED == e.getErrorCode())
+                // actual logon failed (e.g. user account locked)
+                || (SQLServerException.USER_ACCOUNT_LOCKED == e.getErrorCode())
+                // invalid TDS received from server
+                || (SQLServerException.DRIVER_ERROR_INVALID_TDS == e.getDriverErrorCode())
+                // failure negotiating SSL
+                || (SQLServerException.DRIVER_ERROR_SSL_FAILED == e.getDriverErrorCode())
+                // failure TLS1.2
+                || (SQLServerException.DRIVER_ERROR_INTERMITTENT_TLS_FAILED == e.getDriverErrorCode())
+                // unsupported configuration (e.g. Sphinx, invalid packet size, etc.)
+                || (SQLServerException.DRIVER_ERROR_UNSUPPORTED_CONFIG == e.getDriverErrorCode())
+                // no more time to try again
+                || (SQLServerException.ERROR_SOCKET_TIMEOUT == e.getDriverErrorCode()))
+            return true;
+        else
+            return false;
+    }
+
     // reset all params that could have been changed due to ENVCHANGE tokens to defaults,
     // excluding those changed due to routing ENVCHANGE token
     void resetNonRoutingEnvchangeValues() {
@@ -3071,11 +3228,38 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
 
         activeConnectionProperties.remove(SQLServerDriverStringProperty.CLIENT_KEY_PASSWORD.toString());
 
-        // We have successfully connected, now do the login. logon takes seconds timeout
-        executeCommand(new LogonCommand());
+        if (sessionRecovery.getReconnectThread().isAlive()) {
+            if (negotiatedEncryptionLevel != sessionRecovery.getSessionStateTable()
+                    .getOriginalNegotiatedEncryptionLevel()) {
+                connectionlogger.warning(toString()
+                        + " The server did not preserve SSL encryption during a recovery attempt, connection recovery is not possible.");
+                terminate(SQLServerException.DRIVER_ERROR_UNSUPPORTED_CONFIG,
+                        SQLServerException.getErrString("R_crClientSSLStateNotRecoverable"));
+                // fails fast similar to pre-login errors.
+            }
+            try {
+                executeReconnect(new LogonCommand());
+            } catch (SQLServerException e) {
+                // Won't fail fast. Back-off reconnection attempts in effect.
+                throw new SQLServerException(SQLServerException.getErrString("R_crServerSessionStateNotRecoverable"),
+                        e);
+            }
+        } else {
+            // We have successfully connected, now do the login. Log on takes seconds timeout
+            if (connectRetryCount > 0 && null == sessionRecovery.getSessionStateTable()) {
+                sessionRecovery.setSessionStateTable(new SessionStateTable());
+                sessionRecovery.getSessionStateTable().setOriginalNegotiatedEncryptionLevel(negotiatedEncryptionLevel);
+            }
+            executeCommand(new LogonCommand());
+        }
+
         return inetSocketAddress;
     }
 
+    private void executeReconnect(LogonCommand logonCommand) throws SQLServerException {
+        logonCommand.execute(tdsChannel.getWriter(), tdsChannel.getReader(logonCommand));
+    }
+
     /**
      * Negotiates prelogin information with the server.
      */
@@ -3086,7 +3270,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
             fedAuthRequiredByUser = true;
         }
 
-        // Message length (incl. header)
+        // Message length (including header)
         final byte messageLength;
         final byte fedAuthOffset;
         if (fedAuthRequiredByUser) {
@@ -3139,14 +3323,11 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         // PL_OPTION_DATA
         byte[] preloginOptionData = {
                 // Driver major and minor version, 1 byte each
-                (byte) SQLJdbcVersion.major,
-                (byte) SQLJdbcVersion.minor,
+                (byte) SQLJdbcVersion.major, (byte) SQLJdbcVersion.minor,
                 // Revision (Big Endian), 2 bytes
-                (byte) ((SQLJdbcVersion.patch & 0xff00) >> 8),
-                (byte) (SQLJdbcVersion.patch & 0xff),
+                (byte) ((SQLJdbcVersion.patch & 0xff00) >> 8), (byte) (SQLJdbcVersion.patch & 0xff),
                 // Build (Little Endian), 2 bytes
-                (byte) (SQLJdbcVersion.build & 0xff),
-                (byte) ((SQLJdbcVersion.build & 0xff00) >> 8),
+                (byte) (SQLJdbcVersion.build & 0xff), (byte) ((SQLJdbcVersion.build & 0xff00) >> 8),
 
                 // - Encryption -
                 (null == clientCertificate) ? requestedEncryptionLevel
@@ -3576,6 +3757,50 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
              * Add Counter reference to newCommand
              */
             newCommand.createCounter(previousCounter, activeConnectionProperties);
+            if (!(newCommand instanceof LogonCommand)) {
+                // isAlive() doesn't guarantee the thread is actually running, just that it's been requested to start
+                if (!sessionRecovery.getReconnectThread().isAlive()) {
+                    if (this.connectRetryCount > 0 && sessionRecovery.isConnectionRecoveryNegotiated()) {
+                        if (isConnectionDead()) {
+                            if (connectionlogger.isLoggable(Level.FINER)) {
+                                connectionlogger.finer(this.toString() + " Connection is detected to be broken.");
+                            }
+                            if (!sessionRecovery.isConnectionRecoveryPossible()
+                                    || sessionRecovery.getUnprocessedResponseCount() != 0) {
+                                SQLServerException.makeFromDriverError(this, this,
+                                        SQLServerException.getErrString("R_crClientUnrecoverable"), null, false);
+                            }
+                            if (!sessionRecovery.getSessionStateTable().isSessionRecoverable()) {
+                                SQLServerException.makeFromDriverError(this, this,
+                                        SQLServerException.getErrString("R_crServerSessionStateNotRecoverable"), null,
+                                        false);
+                            }
+                            sessionRecovery.getReconnectThread().init(newCommand);
+                            sessionRecovery.getReconnectThread().start();
+                            /*
+                             * Join only blocks the thread that started the reconnect. Currently can't think of a good
+                             * reason to leave the original thread running, no work can be done while we're not
+                             * connected anyways. Can be easily changed to non-blocking if necessary.
+                             */
+                            try {
+                                sessionRecovery.getReconnectThread().join();
+                            } catch (InterruptedException e) {
+                                // Keep compiler happy, something's probably seriously wrong if this line is run
+                                SQLServerException.makeFromDriverError(this, sessionRecovery.getReconnectThread(),
+                                        e.getMessage(), null, false);
+                            }
+                            if (sessionRecovery.getReconnectThread().getException() != null) {
+                                if (connectionlogger.isLoggable(Level.FINER)) {
+                                    connectionlogger.finer(
+                                            this.toString() + "Connection is broken and recovery is not possible.");
+                                }
+                                throw sessionRecovery.getReconnectThread().getException();
+                            }
+                        }
+                    }
+                }
+            }
+
             /*
              * The implementation of this scheduler is pretty simple... Since only one command at a time may use a
              * connection (to avoid TDS protocol errors), just synchronize to serialize command execution.
@@ -3587,7 +3812,11 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                 /*
                  * If execution of the new command left response bytes on the wire (e.g. a large ResultSet or complex
                  * response with multiple results) then remember it as the current command so that any subsequent call
-                 * to executeCommand will detach it before executing another new command.
+                 * to executeCommand will detach it before executing another new command. We should never displace an
+                 * existing currentCommand assert null == currentCommand; If execution of the new command left response
+                 * bytes on the wire (e.g. a large ResultSet or complex response with multiple results) then remember it
+                 * as the current command so that any subsequent call to executeCommand will detach it before executing
+                 * another new command.
                  */
                 if (!commandComplete && !isSessionUnAvailable())
                     currentCommand = newCommand;
@@ -3604,7 +3833,72 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         }
     }
 
+    boolean isConnectionDead() throws SQLServerException {
+        // networkSocketStillConnected would affect perf if called every time. Only run
+        // when the connection has not seen recent activity.
+        if (!idleNetworkTracker.isIdle()) {
+            if (connectionlogger.isLoggable(Level.FINEST)) {
+                connectionlogger.finest(toString() + " Network not idle. Skipping networkSocketStillConnected check.");
+            }
+            return false;
+        }
+        // Only one thread should ever try to perform an idle check on a
+        // disconnected connection at a time.
+        synchronized (this) {
+            // check again if connection is reset already.
+            if (!idleNetworkTracker.isIdle()) {
+                if (connectionlogger.isLoggable(Level.FINEST)) {
+                    connectionlogger
+                            .finest(toString() + " Network not idle. Skipping networkSocketStillConnected check.");
+                }
+                return false;
+            }
+
+            if (isSessionUnAvailable()) {
+                SQLServerException.makeFromDriverError(null, null,
+                        SQLServerException.getErrString("R_connectionIsClosed"),
+                        SQLServerException.EXCEPTION_XOPEN_CONNECTION_FAILURE, false);
+            }
+
+            return !tdsChannel.networkSocketStillConnected();
+        }
+    }
+
     /**
+     * executeCommand without reconnection logic. Only used by the reconnect thread to avoid a lock.
+     */
+    synchronized boolean executeReconnectCommand(TDSCommand newCommand) throws SQLServerException {
+        /*
+         * Detach (buffer) the response from any previously executing command so that we can execute the new command.
+         * Note that detaching the response does not process it. Detaching just buffers the response off of the wire to
+         * clear the TDS channel.
+         */
+        if (null != currentCommand) {
+            currentCommand.detach();
+            currentCommand = null;
+        }
+
+        /*
+         * The implementation of this scheduler is pretty simple... Since only one command at a time may use a
+         * connection (to avoid TDS protocol errors), just synchronize to serialize command execution.
+         */
+        boolean commandComplete = false;
+        try {
+            commandComplete = newCommand.execute(tdsChannel.getWriter(), tdsChannel.getReader(newCommand));
+        } finally {
+            /*
+             * We should never displace an existing currentCommand assert null == currentCommand; If execution of the
+             * new command left response bytes on the wire (e.g. a large ResultSet or complex response with multiple
+             * results) then remember it as the current command so that any subsequent call to executeCommand will
+             * detach it before executing another new command.
+             */
+            if (!commandComplete && !isSessionUnAvailable())
+                currentCommand = newCommand;
+        }
+        return commandComplete;
+    }
+
+    /*
      * Executes a connection-level command
      */
     private void connectionCommand(String sql, String logContext) throws SQLServerException {
@@ -3629,6 +3923,12 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
             }
         }
         executeCommand(new ConnectionCommand(sql, logContext));
+
+        if (sessionRecovery.getReconnectThread().isAlive()) {
+            executeReconnectCommand(new ConnectionCommand(sql, logContext));
+        } else {
+            executeCommand(new ConnectionCommand(sql, logContext));
+        }
     }
 
     /**
@@ -3648,7 +3948,6 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
      * 
      * @param sDB
      *        the new catalog
-     * @return the required syntax
      */
     void setCatalogName(String sDB) {
         if (sDB != null) {
@@ -3658,6 +3957,20 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         }
     }
 
+    /**
+     * Sets the syntax to set the language to use.
+     *
+     * @param language
+     *        the new language
+     */
+    void setLanguageName(String language) {
+        if (language != null) {
+            if (language.length() > 0) {
+                sLanguage = language;
+            }
+        }
+    }
+
     /**
      * Returns the syntax to set the database isolation level.
      * 
@@ -4308,6 +4621,109 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         return len;
     }
 
+    int writeIdleConnectionResiliencyRequest(boolean write, TDSWriter tdsWriter) throws SQLServerException {
+        SessionStateTable ssTable = sessionRecovery.getSessionStateTable();
+        int len = 1;
+        if (write) {
+            tdsWriter.writeByte(TDS.TDS_FEATURE_EXT_SESSIONRECOVERY);
+        }
+        if (!sessionRecovery.getReconnectThread().isAlive()) {
+            if (write) {
+                tdsWriter.writeInt(0);
+            }
+            len += 4;
+        } else {
+            int initialLength = 0;
+            initialLength += 1 + 2 * ssTable.getOriginalCatalog().length();
+            initialLength += 1 + 2 * ssTable.getOriginalLanguage().length();
+            initialLength += 1 + (ssTable.getOriginalCollation() == null ? 0 : SQLCollation.tdsLength());
+            initialLength += ssTable.getInitialLength();
+
+            int currentLength = 0;
+            currentLength += 1 + 2 * (sCatalog.equals(ssTable.getOriginalCatalog()) ? 0 : sCatalog.length());
+            currentLength += 1 + 2 * (sLanguage.equals(ssTable.getOriginalLanguage()) ? 0 : sLanguage.length());
+            currentLength += 1 + (databaseCollation == null
+                    || databaseCollation.isEqual(ssTable.getOriginalCollation()) ? 0 : SQLCollation.tdsLength());
+            currentLength += ssTable.getDeltaLength();
+
+            if (write) {
+                // length of data w/o total length (initial + current + 2 * sizeof(DWORD))
+                tdsWriter.writeInt(8 + initialLength + currentLength);
+                tdsWriter.writeInt(initialLength);
+                tdsWriter.writeByte((byte) ssTable.getOriginalCatalog().length());
+                tdsWriter.writeBytes(toUCS16(ssTable.getOriginalCatalog()));
+                if (ssTable.getOriginalCollation() != null) {
+                    tdsWriter.writeByte((byte) SQLCollation.tdsLength());
+                    ssTable.getOriginalCollation().writeCollation(tdsWriter);
+                } else {
+                    tdsWriter.writeByte((byte) 0); // collation length
+                }
+                tdsWriter.writeByte((byte) ssTable.getOriginalLanguage().length());
+                tdsWriter.writeBytes(toUCS16(ssTable.getOriginalLanguage()));
+                for (int i = 0; i < SessionStateTable.SESSION_STATE_ID_MAX; i++) {
+                    if (ssTable.getSessionStateInitial()[i] != null) {
+                        tdsWriter.writeByte((byte) i); // state id
+                        if (ssTable.getSessionStateInitial()[i].length >= 0xFF) {
+                            tdsWriter.writeByte((byte) 0xFF);
+                            tdsWriter.writeShort((short) ssTable.getSessionStateInitial()[i].length);
+                        } else {
+                            tdsWriter.writeByte((byte) (ssTable.getSessionStateInitial()[i]).length); // state length
+                        }
+                        tdsWriter.writeBytes(ssTable.getSessionStateInitial()[i]); // state value
+                    }
+                }
+                tdsWriter.writeInt(currentLength);
+
+                if (ssTable.spResetCalled()) {
+                    sCatalog = ssTable.getOriginalCatalog();
+                    databaseCollation = ssTable.getOriginalCollation();
+                    sLanguage = ssTable.getOriginalLanguage();
+                    ssTable.setspResetCalled(false);
+                }
+
+                // database/catalog
+                if (sCatalog.equals(ssTable.getOriginalCatalog())) {
+                    tdsWriter.writeByte((byte) 0);
+                } else {
+                    tdsWriter.writeByte((byte) sCatalog.length());
+                    tdsWriter.writeBytes(toUCS16(sCatalog));
+                }
+
+                // collation
+                if (databaseCollation == null || databaseCollation.isEqual(ssTable.getOriginalCollation())) {
+                    tdsWriter.writeByte((byte) 0);
+                } else {
+                    tdsWriter.writeByte((byte) SQLCollation.tdsLength());
+                    databaseCollation.writeCollation(tdsWriter);
+                }
+
+                // language
+                if (sLanguage.equals(ssTable.getOriginalLanguage())) {
+                    tdsWriter.writeByte((byte) 0);
+                } else {
+                    tdsWriter.writeByte((byte) sLanguage.length());
+                    tdsWriter.writeBytes(toUCS16(sLanguage));
+                }
+
+                // Delta session state
+                for (int i = 0; i < SessionStateTable.SESSION_STATE_ID_MAX; i++) {
+                    if (ssTable.getSessionStateDelta()[i] != null
+                            && ssTable.getSessionStateDelta()[i].getData() != null) {
+                        tdsWriter.writeByte((byte) i); // state id
+                        if (ssTable.getSessionStateDelta()[i].getDataLength() >= 0xFF) {
+                            tdsWriter.writeByte((byte) 0xFF);
+                            tdsWriter.writeShort((short) ssTable.getSessionStateDelta()[i].getDataLength());
+                        } else
+                            tdsWriter.writeByte((byte) (ssTable.getSessionStateDelta()[i].getDataLength()));
+                        tdsWriter.writeBytes(ssTable.getSessionStateDelta()[i].getData()); // state value
+                    }
+                }
+            }
+            len += initialLength + currentLength + 12 /* length fields (initial, current, total) */;
+        }
+        return len;
+    }
+
     /**
      * Logon Command
      *
@@ -4525,15 +4941,19 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
             case ENVCHANGE_CHANGE_MIRROR:
                 setFailoverPartnerServerProvided(tdsReader.readUnicodeString(tdsReader.readUnsignedByte()));
                 break;
-            // Skip unsupported, ENVCHANGES
             case ENVCHANGE_LANGUAGE:
+                setLanguageName(tdsReader.readUnicodeString(tdsReader.readUnsignedByte()));
+                break;
+            case ENVCHANGE_RESET_COMPLETE:
+                sessionRecovery.getSessionStateTable().reset();
+                break;
+            // Skip unsupported, ENVCHANGES
             case ENVCHANGE_CHARSET:
             case ENVCHANGE_SORTLOCALEID:
             case ENVCHANGE_SORTFLAGS:
             case ENVCHANGE_DTC_PROMOTE:
             case ENVCHANGE_DTC_MGR_ADDR:
             case ENVCHANGE_XACT_ENDED:
-            case ENVCHANGE_RESET_COMPLETE:
             case ENVCHANGE_USER_INFO:
                 if (connectionlogger.isLoggable(Level.FINER))
                     connectionlogger.finer(toString() + " Ignored env change: " + envchange);
@@ -4780,6 +5200,75 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         activeConnectionProperties.remove(SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_SECRET.toString());
     }
 
+    final void processSessionState(TDSReader tdsReader) throws SQLServerException {
+        if (sessionRecovery.isConnectionRecoveryNegotiated()) {
+            tdsReader.readUnsignedByte(); // token type
+            long dataLength = tdsReader.readUnsignedInt();
+            if (dataLength < 7) {
+                if (connectionlogger.isLoggable(Level.SEVERE))
+                    connectionlogger.severe(toString()
+                            + "SESSIONSTATETOKEN token stream is not long enough to contain the data it claims to.");
+                sessionRecovery.getSessionStateTable().setMasterRecoveryDisabled(true);
+                tdsReader.throwInvalidTDS();
+            }
+            int sequenceNumber = tdsReader.readInt();
+            long dataBytesRead = 4;
+            /*
+             * Sequence number has reached max value and will now roll over. Hence disable CR permanently. This is set
+             * to false when session state data is being reset to initial state when connection is taken out of a
+             * connection pool.
+             */
+            if (SessionStateTable.MASTER_RECOVERY_DISABLE_SEQ_NUMBER == sequenceNumber) {
+                sessionRecovery.getSessionStateTable().setMasterRecoveryDisabled(true);
+            }
+
+            byte status = (byte) tdsReader.readUnsignedByte();
+            boolean fRecoverable = (status & 0x01) > 0 ? true : false;
+            dataBytesRead += 1;
+
+            while (dataBytesRead < dataLength) {
+                short sessionStateId = (short) tdsReader.readUnsignedByte();
+                int sessionStateLength = (int) tdsReader.readUnsignedByte();
+                dataBytesRead += 2;
+                if (sessionStateLength >= 0xFF) {
+                    sessionStateLength = (int) tdsReader.readUnsignedInt(); // xFF - xFFFF
+                    dataBytesRead += 4;
+                }
+
+                if (sessionRecovery.getSessionStateTable().getSessionStateDelta()[sessionStateId] == null) {
+                    sessionRecovery.getSessionStateTable()
+                            .getSessionStateDelta()[sessionStateId] = new SessionStateValue();
+                }
+                /*
+                 * else Exception will not be thrown. Instead the state is just ignored.
+                 */
+
+                if (SessionStateTable.MASTER_RECOVERY_DISABLE_SEQ_NUMBER != sequenceNumber
+                        && ((null == sessionRecovery.getSessionStateTable().getSessionStateDelta()[sessionStateId]
+                                .getData())
+                                || (sessionRecovery.getSessionStateTable().getSessionStateDelta()[sessionStateId]
+                                        .isSequenceNumberGreater(sequenceNumber)))) {
+                    sessionRecovery.getSessionStateTable().updateSessionState(tdsReader, sessionStateId,
+                            sessionStateLength, sequenceNumber, fRecoverable);
+                } else {
+                    tdsReader.readSkipBytes(sessionStateLength);
+                }
+                dataBytesRead += sessionStateLength;
+            }
+            if (dataBytesRead != dataLength) {
+                if (connectionlogger.isLoggable(Level.SEVERE))
+                    connectionlogger.severe(toString() + " Session State data length is corrupt.");
+                sessionRecovery.getSessionStateTable().setMasterRecoveryDisabled(true);
+                tdsReader.throwInvalidTDS();
+            }
+        } else {
+            if (connectionlogger.isLoggable(Level.SEVERE))
+                connectionlogger
+                        .severe(toString() + " Session state received when session recovery was not negotiated.");
+            tdsReader.throwInvalidTDSToken(TDS.getTokenName(tdsReader.peekTokenType()));
+        }
+    }
+
     /**
      * FedAuth Token Command
      *
@@ -5030,9 +5519,8 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
 
     private void onFeatureExtAck(byte featureId, byte[] data) throws SQLServerException {
         // To be able to cache both control and tenant ring IPs, need to parse AZURESQLDNSCACHING.
-        if (null != routingInfo && TDS.TDS_FEATURE_EXT_AZURESQLDNSCACHING != featureId) {
+        if (null != routingInfo && TDS.TDS_FEATURE_EXT_AZURESQLDNSCACHING != featureId)
             return;
-        }
 
         switch (featureId) {
             case TDS.TDS_FEATURE_EXT_FEDAUTH: {
@@ -5145,6 +5633,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                 }
                 break;
             }
+
             case TDS.TDS_FEATURE_EXT_AZURESQLDNSCACHING: {
                 if (connectionlogger.isLoggable(Level.FINER)) {
                     connectionlogger.fine(
@@ -5169,6 +5658,18 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                 }
                 break;
             }
+
+            case TDS.TDS_FEATURE_EXT_SESSIONRECOVERY: {
+                if (connectionlogger.isLoggable(Level.FINER)) {
+                    connectionlogger.fine(
+                            toString() + " Received feature extension acknowledgement for Idle Connection Resiliency.");
+                }
+                sessionRecovery.parseInitialSessionStateData(data,
+                        sessionRecovery.getSessionStateTable().getSessionStateInitial());
+                sessionRecovery.setConnectionRecoveryNegotiated(true);
+                sessionRecovery.setConnectionRecoveryPossible(true);
+                break;
+            }
             default: {
                 // Unknown feature ack
                 throw new SQLServerException(SQLServerException.getErrString("R_UnknownFeatureAck"), null);
@@ -5457,6 +5958,11 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
 
         len = len + 1; // add 1 to length because of FeatureEx terminator
 
+        // Idle Connection Resiliency is requested
+        if (connectRetryCount > 0) {
+            len = len + writeIdleConnectionResiliencyRequest(false, tdsWriter);
+        }
+
         // Length of entire Login 7 packet
         tdsWriter.writeInt(len);
         tdsWriter.writeInt(tdsVersion);
@@ -5647,6 +6153,11 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         writeUTF8SupportFeatureRequest(true, tdsWriter);
         writeDNSCacheFeatureRequest(true, tdsWriter);
 
+        // Idle Connection Resiliency is requested
+        if (connectRetryCount > 0) {
+            writeIdleConnectionResiliencyRequest(true, tdsWriter);
+        }
+
         tdsWriter.writeByte((byte) TDS.FEATURE_EXT_TERMINATOR);
         tdsWriter.setDataLoggable(true);
 
@@ -5654,8 +6165,23 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         TDSReader tdsReader;
         do {
             tdsReader = logonCommand.startResponse();
+            sessionRecovery.setConnectionRecoveryPossible(false);
             TDSParser.parse(tdsReader, logonProcessor);
         } while (!logonProcessor.complete(logonCommand, tdsReader));
+
+        if (sessionRecovery.getReconnectThread().isAlive() && !sessionRecovery.isConnectionRecoveryPossible()) {
+            if (connectionlogger.isLoggable(Level.WARNING)) {
+                connectionlogger.warning(this.toString()
+                        + "SessionRecovery feature extension ack was not sent by the server during reconnection.");
+            }
+            terminate(SQLServerException.DRIVER_ERROR_INVALID_TDS,
+                    SQLServerException.getErrString("R_crClientNoRecoveryAckFromLogin"));
+        }
+        if (connectRetryCount > 0 && !sessionRecovery.getReconnectThread().isAlive()) {
+            sessionRecovery.getSessionStateTable().setOriginalCatalog(sCatalog);
+            sessionRecovery.getSessionStateTable().setOriginalCollation(databaseCollation);
+            sessionRecovery.getSessionStateTable().setOriginalLanguage(sLanguage);
+        }
     }
 
     /* --------------- JDBC 3.0 ------------- */
@@ -6628,8 +7154,9 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
 
     /**
      * Sets time-to-live for column encryption key entries in the column encryption key cache for the Always Encrypted
-     * feature. The default value is 2 hours. This variable holds the value in seconds. This only applies to global-level
-     * key store providers. Connection and Statement-level providers need to set their own cache TTL values.
+     * feature. The default value is 2 hours. This variable holds the value in seconds. This only applies to
+     * global-level key store providers. Connection and Statement-level providers need to set their own cache TTL
+     * values.
      * 
      * @param columnEncryptionKeyCacheTTL
      *        The timeunit in seconds
@@ -7007,12 +7534,14 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
     /** Enclave provider */
     private ISQLServerEnclaveProvider enclaveProvider;
 
-    ArrayList<byte[]> initEnclaveParameters(SQLServerStatement statement, String userSql, String preparedTypeDefinitions, Parameter[] params,
+    ArrayList<byte[]> initEnclaveParameters(SQLServerStatement statement, String userSql,
+            String preparedTypeDefinitions, Parameter[] params,
             ArrayList<String> parameterNames) throws SQLServerException {
         if (!this.enclaveEstablished()) {
             enclaveProvider.getAttestationParameters(this.enclaveAttestationUrl);
         }
-        return enclaveProvider.createEnclaveSession(this, statement, userSql, preparedTypeDefinitions, params, parameterNames);
+        return enclaveProvider.createEnclaveSession(this, statement, userSql, preparedTypeDefinitions, params,
+                parameterNames);
     }
 
     boolean enclaveEstablished() {
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java
index abcdf660..ab43fa60 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java
@@ -450,7 +450,9 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
 
     @Override
     public boolean supportsSharding() throws SQLException {
-        DriverJDBCVersion.checkSupportsJDBC43();
+        if (!DriverJDBCVersion.checkSupportsJDBC43()) {
+            throw new UnsupportedOperationException(SQLServerException.getErrString("R_notSupported"));
+        }
         checkClosed();
         return false;
     }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
index 4848e657..e4942221 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
@@ -44,8 +44,8 @@ final class SQLServerDriverPropertyInfo {
     }
 
     DriverPropertyInfo build(Properties connProperties) {
-        String propValue = name.equals(SQLServerDriverStringProperty.PASSWORD.toString()) ? "" : connProperties
-                .getProperty(name);
+        String propValue = name
+                .equals(SQLServerDriverStringProperty.PASSWORD.toString()) ? "" : connProperties.getProperty(name);
 
         if (null == propValue)
             propValue = defaultValue;
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerJdbc42.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerJdbc42.java
index aad39037..4ecb29d3 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerJdbc42.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerJdbc42.java
@@ -1,20 +1,21 @@
 /*
- * Microsoft JDBC Driver for SQL Server
- * 
- * Copyright(c) Microsoft Corporation All rights reserved.
- * 
- * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
  */
 
 package com.microsoft.sqlserver.jdbc;
 
+import java.net.Socket;
 import java.sql.BatchUpdateException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
 
 /**
  * Shims for JDBC 4.2 JAR.
  *
- * JDBC 4.2 public methods should always check the SQLServerJdbcVersion first to make sure that they are not operable in any earlier driver version.
- * That is, they should throw an exception, be a no-op, or whatever.
+ * JDBC 4.2 public methods should always check the SQLServerJdbcVersion first to make sure that they are not operable in
+ * any earlier driver version. That is, they should throw an exception, be a no-op, or whatever.
  */
 
 final class DriverJDBCVersion {
@@ -22,14 +23,16 @@ final class DriverJDBCVersion {
     static final int major = 4;
     static final int minor = 2;
 
-    static final void checkSupportsJDBC43() {
-        throw new UnsupportedOperationException(SQLServerException.getErrString("R_notSupported"));
+    private static final Logger logger = Logger.getLogger("com.microsoft.sqlserver.jdbc.internals.DriverJDBCVersion");
+
+    static final boolean checkSupportsJDBC43() {
+        return false;
     }
 
     static final void throwBatchUpdateException(SQLServerException lastError,
             long[] updateCounts) throws BatchUpdateException {
-        throw new BatchUpdateException(lastError.getMessage(), lastError.getSQLState(), lastError.getErrorCode(), updateCounts,
-                new Throwable(lastError.getMessage()));
+        throw new BatchUpdateException(lastError.getMessage(), lastError.getSQLState(), lastError.getErrorCode(),
+                updateCounts, new Throwable(lastError.getMessage()));
     }
 
     static SQLServerConnection getSQLServerConnection(String parentInfo) throws SQLServerException {
@@ -42,4 +45,11 @@ final class DriverJDBCVersion {
     static int getProcessId() {
         return pid;
     }
-}
\ No newline at end of file
+
+    static void setSocketOptions(Socket tcpSocket, TDSChannel channel) {
+        if (logger.isLoggable(Level.FINER)) {
+            logger.finer(
+                    "Socket.supportedOptions() not available on this JVM. Extended KeepAlive options will not be set.");
+        }
+    }
+}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerJdbc43.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerJdbc43.java
index 645e6729..a347c048 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerJdbc43.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerJdbc43.java
@@ -5,7 +5,13 @@
 
 package com.microsoft.sqlserver.jdbc;
 
+import java.io.IOException;
+import java.net.Socket;
+import java.net.SocketOption;
 import java.sql.BatchUpdateException;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 
 /**
@@ -20,8 +26,10 @@ final class DriverJDBCVersion {
     static final int major = 4;
     static final int minor = 3;
 
-    static final void checkSupportsJDBC43() {
-        return;
+    private static final Logger logger = Logger.getLogger("com.microsoft.sqlserver.jdbc.internals.DriverJDBCVersion");
+
+    static final boolean checkSupportsJDBC43() {
+        return true;
     }
 
     static final void throwBatchUpdateException(SQLServerException lastError,
@@ -51,4 +59,18 @@ final class DriverJDBCVersion {
     static int getProcessId() {
         return pid;
     }
+
+    static void setSocketOptions(Socket tcpSocket, TDSChannel channel) throws IOException {
+        Set<SocketOption<?>> options = tcpSocket.supportedOptions();
+        if (options.contains(ExtendedSocketOptions.TCP_KEEPIDLE)
+                && options.contains(ExtendedSocketOptions.TCP_KEEPINTERVAL)) {
+            if (logger.isLoggable(Level.FINER)) {
+                logger.finer(channel.toString() + ": Setting KeepAlive extended socket options.");
+            }
+            tcpSocket.setOption(ExtendedSocketOptions.TCP_KEEPIDLE, 30); // 30 seconds
+            tcpSocket.setOption(ExtendedSocketOptions.TCP_KEEPINTERVAL, 1); // 1 second
+        } else if (logger.isLoggable(Level.FINER)) {
+            logger.finer(channel.toString() + ": KeepAlive extended socket options not supported on this platform.");
+        }
+    }
 }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
index 02224d0a..4f80d4f4 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
@@ -491,6 +491,15 @@ public final class SQLServerResource extends ListResourceBundle {
         {"R_maxResultBufferInvalidSyntax", "Invalid syntax: {0} in maxResultBuffer parameter."},
         {"R_maxResultBufferNegativeParameterValue", "MaxResultBuffer must have positive value: {0}."},
         {"R_maxResultBufferPropertyExceeded", "MaxResultBuffer property exceeded: {0}. MaxResultBuffer was set to: {1}."},
+        {"R_invalidConnectRetryCount", "Connection retry count {0} is not valid."},
+        {"R_connectRetryCountPropertyDescription", "The maximum number of attempts to reestablish a broken connection."},
+        {"R_invalidConnectRetryInterval", "Connection retry interval {0} is not valid."},
+        {"R_connectRetryIntervalPropertyDescription", "The interval, in seconds, between attempts to reestablish a connection."},
+        {"R_crClientAllRecoveryAttemptsFailed", "The connection is broken and recovery is not possible. The client driver attempted to recover the connection one or more times and all attempts failed. Increase the value of ConnectRetryCount to increase the number of recovery attempts."},
+        {"R_crClientNoRecoveryAckFromLogin", "The server did not acknowledge a recovery attempt, connection recovery is not possible."},
+        {"R_crServerSessionStateNotRecoverable", "The connection is broken and recovery is not possible. The connection is marked by the server as unrecoverable. No attempt was made to restore the connection."},
+        {"R_crClientUnrecoverable","The connection is broken and recovery is not possible. The connection is marked by the client driver as unrecoverable. No attempt was made to restore the connection."},
+        {"R_crClientSSLStateNotRecoverable", "The server did not preserve SSL encryption during a recovery attempt, connection recovery is not possible."},
     };
 }
-// @formatter:on
\ No newline at end of file
+// @formatter:on
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java
index 9f2ca277..2429f06a 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java
@@ -390,6 +390,10 @@ public class SQLServerResultSet implements ISQLServerResultSet, java.io.Serializ
                         // Consume the DONE packet if there is error
                         StreamDone doneToken = new StreamDone();
                         doneToken.setFromTDS(tdsReader);
+                        if (doneToken.isFinal()) {
+                            // Response is completely processed, hence decrement unprocessed response count.
+                            stmt.connection.getSessionRecovery().decrementUnprocessedResponseCount();
+                        }
                         return true;
                     }
                 }
@@ -5380,7 +5384,11 @@ public class SQLServerResultSet implements ISQLServerResultSet, java.io.Serializ
 
                 StreamDone doneToken = new StreamDone();
                 doneToken.setFromTDS(tdsReader);
-
+                if (doneToken.isFinal()) {
+                    // Response is completely processed, hence decrement unprocessed response count.
+                    stmt.connection.getSessionRecovery().decrementUnprocessedResponseCount();
+                }
+                
                 // Done with all the rows in this fetch buffer and done with parsing
                 // unless it's a server cursor, in which case there is a RETSTAT and
                 // another DONE token to follow.
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java
index f0480d4d..75b6bcd4 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java
@@ -74,7 +74,7 @@ public class SQLServerStatement implements ISQLServerStatement {
     }
 
     final static String identityQuery = " select SCOPE_IDENTITY() AS GENERATED_KEYS";
-    
+
     static final String WINDOWS_KEY_STORE_NAME = "MSSQL_CERTIFICATE_STORE";
 
     /** the stored procedure name to call (if there is one) */
@@ -151,6 +151,7 @@ public class SQLServerStatement implements ISQLServerStatement {
             lastStmtExecCmd.close();
             lastStmtExecCmd = null;
         }
+
         clearLastResult();
     }
 
@@ -245,7 +246,8 @@ public class SQLServerStatement implements ISQLServerStatement {
             else
                 throw e;
         } finally {
-            lastStmtExecCmd = newStmtCmd;
+            if (newStmtCmd.wasExecuted())
+                lastStmtExecCmd = newStmtCmd;
         }
     }
 
@@ -409,6 +411,7 @@ public class SQLServerStatement implements ISQLServerStatement {
 
     class StmtExecOutParamHandler extends TDSTokenHandler {
         SQLServerStatement statement;
+
         StmtExecOutParamHandler(SQLServerStatement statement) {
             super("StmtExecOutParamHandler");
             this.statement = statement;
@@ -1478,6 +1481,10 @@ public class SQLServerStatement implements ISQLServerStatement {
                 StreamDone doneToken = new StreamDone();
                 doneToken.setFromTDS(tdsReader);
 
+                if (doneToken.isFinal()) {
+                    // Response is completely processed, hence decrement unprocessed response count.
+                    connection.getSessionRecovery().decrementUnprocessedResponseCount();
+                }
                 // If the done token has the attention ack bit set, then record
                 // it as the attention ack DONE token. We may or may not throw
                 // an statement canceled/timed out exception later based on
@@ -2436,8 +2443,8 @@ public class SQLServerStatement implements ISQLServerStatement {
             }
 
             if (null == entry.getValue()) {
-                throw new SQLServerException(null,
-                        String.format(SQLServerException.getErrString("R_CustomKeyStoreProviderValueNull"), providerName),
+                throw new SQLServerException(null, String
+                        .format(SQLServerException.getErrString("R_CustomKeyStoreProviderValueNull"), providerName),
                         null, 0, false);
             }
 
@@ -2457,15 +2464,16 @@ public class SQLServerStatement implements ISQLServerStatement {
     }
 
     synchronized boolean hasColumnEncryptionKeyStoreProvidersRegistered() {
-        return null != statementColumnEncryptionKeyStoreProviders && statementColumnEncryptionKeyStoreProviders.size() > 0;
+        return null != statementColumnEncryptionKeyStoreProviders
+                && statementColumnEncryptionKeyStoreProviders.size() > 0;
     }
 
     synchronized SQLServerColumnEncryptionKeyStoreProvider getColumnEncryptionKeyStoreProvider(
             String providerName) throws SQLServerException {
 
         // Check for a statement-level provider first
-        if (null != statementColumnEncryptionKeyStoreProviders &&
-                statementColumnEncryptionKeyStoreProviders.size() > 0) {
+        if (null != statementColumnEncryptionKeyStoreProviders
+                && statementColumnEncryptionKeyStoreProviders.size() > 0) {
             // If any statement-level providers are registered, we don't fall back to connection-level providers
             if (statementColumnEncryptionKeyStoreProviders.containsKey(providerName)) {
                 return statementColumnEncryptionKeyStoreProviders.get(providerName);
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/TimeoutCommand.java b/src/main/java/com/microsoft/sqlserver/jdbc/TimeoutCommand.java
new file mode 100644
index 00000000..a72b9c1a
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/TimeoutCommand.java
@@ -0,0 +1,42 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+
+package com.microsoft.sqlserver.jdbc;
+
+/**
+ * Abstract implementation of a command that can be timed out using the {@link TimeoutPoller}
+ */
+abstract class TimeoutCommand<T> {
+    private final long startTime;
+    private final int timeout;
+    private final T command;
+    private final SQLServerConnection sqlServerConnection;
+
+    TimeoutCommand(int timeout, T command, SQLServerConnection sqlServerConnection) {
+        this.timeout = timeout;
+        this.command = command;
+        this.sqlServerConnection = sqlServerConnection;
+        this.startTime = System.currentTimeMillis();
+    }
+
+    protected boolean canTimeout() {
+        long currentTime = System.currentTimeMillis();
+        return ((currentTime - startTime) / 1000) >= timeout;
+    }
+
+    protected T getCommand() {
+        return command;
+    }
+
+    protected SQLServerConnection getSqlServerConnection() {
+        return sqlServerConnection;
+    }
+
+    /**
+     * The implementation for interrupting this timeout command
+     * @throws Exception 
+     */
+    protected abstract void interrupt() throws Exception;
+}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/TimeoutPoller.java b/src/main/java/com/microsoft/sqlserver/jdbc/TimeoutPoller.java
new file mode 100644
index 00000000..6c53d4d7
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/TimeoutPoller.java
@@ -0,0 +1,82 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+
+package com.microsoft.sqlserver.jdbc;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+/**
+ * Thread that runs in the background while the mssql driver is used that can timeout TDSCommands Checks all registered
+ * commands every second to see if they can be interrupted
+ */
+final class TimeoutPoller implements Runnable {
+    private List<TimeoutCommand<TDSCommand>> timeoutCommands = new ArrayList<>();
+    final static Logger logger = Logger.getLogger("com.microsoft.sqlserver.jdbc.TimeoutPoller");
+    private static volatile TimeoutPoller timeoutPoller = null;
+
+    static TimeoutPoller getTimeoutPoller() {
+        if (timeoutPoller == null) {
+            synchronized (TimeoutPoller.class) {
+                if (timeoutPoller == null) {
+                    // initialize the timeout poller thread once
+                    timeoutPoller = new TimeoutPoller();
+                    // start the timeout polling thread
+                    Thread pollerThread = new Thread(timeoutPoller, "mssql-jdbc-TimeoutPoller");
+                    pollerThread.setDaemon(true);
+                    pollerThread.start();
+                }
+            }
+        }
+        return timeoutPoller;
+    }
+
+    void addTimeoutCommand(TimeoutCommand<TDSCommand> timeoutCommand) {
+        synchronized (timeoutCommands) {
+            timeoutCommands.add(timeoutCommand);
+        }
+    }
+
+    void remove(TimeoutCommand<TDSCommand> timeoutCommand) {
+        synchronized (timeoutCommands) {
+            timeoutCommands.remove(timeoutCommand);
+        }
+    }
+
+    private TimeoutPoller() {}
+
+    public void run() {
+        try {
+            // Poll every second checking for commands that have timed out and need
+            // interruption
+            while (true) {
+                synchronized (timeoutCommands) {
+                    Iterator<TimeoutCommand<TDSCommand>> timeoutCommandIterator = timeoutCommands.iterator();
+                    while (timeoutCommandIterator.hasNext()) {
+                        TimeoutCommand<TDSCommand> timeoutCommand = timeoutCommandIterator.next();
+                        try {
+                            if (timeoutCommand.canTimeout()) {
+                                try {
+                                    timeoutCommand.interrupt();
+                                } finally {
+                                    timeoutCommandIterator.remove();
+                                }
+                            }
+                        } catch (Exception e) {
+                            logger.log(Level.WARNING, "Could not timeout command", e);
+                        }
+                    }
+                }
+                Thread.sleep(1000);
+            }
+        } catch (Exception e) {
+            logger.log(Level.SEVERE, "Error processing timeout commands", e);
+        }
+    }
+}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java b/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java
index edf23233..150fcde6 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java
@@ -83,6 +83,9 @@ final class TDSParser {
                 case TDS.TDS_ENV_CHG:
                     parsing = tdsTokenHandler.onEnvChange(tdsReader);
                     break;
+                case TDS.TDS_SESSION_STATE:
+                    parsing = tdsTokenHandler.onSessionState(tdsReader);
+                    break;
                 case TDS.TDS_RET_STAT:
                     parsing = tdsTokenHandler.onRetStatus(tdsReader);
                     break;
@@ -213,6 +216,11 @@ class TDSTokenHandler {
         return true;
     }
 
+    boolean onSessionState(TDSReader tdsReader) throws SQLServerException {
+        tdsReader.getConnection().processSessionState(tdsReader);
+        return true;
+    }
+
     boolean onRetStatus(TDSReader tdsReader) throws SQLServerException {
         (new StreamRetStatus()).setFromTDS(tdsReader);
         return true;
@@ -226,6 +234,10 @@ class TDSTokenHandler {
     boolean onDone(TDSReader tdsReader) throws SQLServerException {
         StreamDone doneToken = new StreamDone();
         doneToken.setFromTDS(tdsReader);
+        if (doneToken.isFinal()) {
+            // Response is completely processed hence decrement unprocessed response count.
+            tdsReader.getConnection().getSessionRecovery().decrementUnprocessedResponseCount();
+        }
         return true;
     }
 


Test patch:
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java
index 697d35ea..0787f6fe 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java
@@ -29,7 +29,6 @@ import java.util.Calendar;
 import java.util.List;
 import java.util.Locale;
 import java.util.ResourceBundle;
-
 import com.microsoft.sqlserver.testframework.AbstractSQLGenerator;
 import com.microsoft.sqlserver.testframework.PrepUtil;
 import com.microsoft.sqlserver.testframework.sqlType.SqlBigInt;
@@ -75,11 +74,26 @@ public final class TestUtils {
     static final int ENGINE_EDITION_FOR_SQL_AZURE_DW = 6;
     static final int ENGINE_EDITION_FOR_SQL_AZURE_MI = 8;
 
-    private static Boolean isAzure = null;
-    private static Boolean isAzureDW = null;
-    private static Boolean isAzureMI = null;
-
     private TestUtils() {}
+    
+    /**
+     * Checks if the connection session recovery object has negotiated reflection.
+     * @param con
+     * @return
+     */
+    public static boolean isConnectionRecoveryNegotiated(Connection con) {
+        return ((SQLServerConnection) con).getSessionRecovery().isConnectionRecoveryNegotiated();
+    }
+    
+    /**
+     * Checks if connection is dead.
+     * @param con
+     * @return
+     * @throws SQLServerException
+     */
+    public static boolean isConnectionDead(Connection con) throws SQLServerException {
+        return ((SQLServerConnection) con).isConnectionDead();
+    }
 
     /**
      * Checks if connection is established to Azure server.
@@ -97,7 +111,7 @@ public final class TestUtils {
      */
     public static boolean isAzureDW(Connection con) {
         isAzure(con);
-        return isAzureDW;
+        return ((SQLServerConnection) con).isAzureDW();
     }
 
     /**
@@ -107,7 +121,7 @@ public final class TestUtils {
      */
     public static boolean isAzureMI(Connection con) {
         isAzure(con);
-        return isAzureMI;
+        return ((SQLServerConnection) con).isAzureMI();
     }
 
     /**
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java
new file mode 100644
index 00000000..3cfc345e
--- /dev/null
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java
@@ -0,0 +1,261 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+
+package com.microsoft.sqlserver.jdbc.resiliency;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import javax.sql.PooledConnection;
+
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import com.microsoft.sqlserver.jdbc.RandomUtil;
+import com.microsoft.sqlserver.jdbc.SQLServerConnectionPoolDataSource;
+import com.microsoft.sqlserver.jdbc.TestResource;
+import com.microsoft.sqlserver.jdbc.TestUtils;
+import com.microsoft.sqlserver.testframework.AbstractTest;
+import com.microsoft.sqlserver.testframework.Constants;
+
+
+@Tag(Constants.xSQLv11)
+public class BasicConnectionTest extends AbstractTest {
+
+    @Test
+    public void testBasicReconnectDefault() throws SQLException {
+        basicReconnect(connectionString);
+    }
+
+    @Test
+    public void testBasicEncryptedConnection() throws SQLException {
+        basicReconnect(connectionString + ";encrypt=true;trustServerCertificate=true;");
+    }
+
+    @Test
+    public void testGracefulClose() throws SQLException {
+        try (Connection c = ResiliencyUtils.getConnection(connectionString)) {
+            try (Statement s = c.createStatement()) {
+                ResiliencyUtils.killConnection(c, connectionString);
+                c.close();
+                s.executeQuery("SELECT 1");
+                fail("Query execution did not throw an exception on a closed execution");
+            } catch (SQLException e) {
+                String message = e.getMessage();
+                assertEquals(TestResource.getResource("R_connectionIsClosed"), message);
+            }
+        }
+    }
+
+    @Test
+    @Tag(Constants.xAzureSQLDB) // Switching databases is not supported against Azure, skip/
+    @Tag(Constants.xAzureSQLDW)
+    public void testCatalog() throws SQLException {
+        String expectedDatabaseName = null;
+        String actualDatabaseName = null;
+        try (Connection c = ResiliencyUtils.getConnection(connectionString); Statement s = c.createStatement()) {
+            expectedDatabaseName = RandomUtil.getIdentifier("resDB");
+            TestUtils.dropDatabaseIfExists(expectedDatabaseName, connectionString);
+            s.execute("CREATE DATABASE [" + expectedDatabaseName + "]");
+            try {
+                c.setCatalog(expectedDatabaseName);
+            } catch (SQLException e) {
+                return;
+            }
+            ResiliencyUtils.killConnection(c, connectionString);
+            try (ResultSet rs = s.executeQuery("SELECT db_name();")) {
+                while (rs.next()) {
+                    actualDatabaseName = rs.getString(1);
+                }
+            }
+            // Check if the driver reconnected to the expected database.
+            assertEquals(expectedDatabaseName, actualDatabaseName);
+        } finally {
+            TestUtils.dropDatabaseIfExists(expectedDatabaseName, connectionString);
+        }
+    }
+    
+    @Test
+    @Tag(Constants.xAzureSQLDB) // Switching databases is not supported against Azure, skip/
+    @Tag(Constants.xAzureSQLDW)
+    public void testUseDb() throws SQLException {
+        String expectedDatabaseName = null;
+        String actualDatabaseName = null;
+        try (Connection c = ResiliencyUtils.getConnection(connectionString); Statement s = c.createStatement()) {
+            expectedDatabaseName = RandomUtil.getIdentifier("resDB");
+            TestUtils.dropDatabaseIfExists(expectedDatabaseName, connectionString);
+            s.execute("CREATE DATABASE [" + expectedDatabaseName + "]");
+            try {
+                s.execute("USE [" + expectedDatabaseName + "]");
+            } catch (SQLException e) {
+                return;
+            }
+            ResiliencyUtils.killConnection(c, connectionString);
+            try (ResultSet rs = s.executeQuery("SELECT db_name();")) {
+                while (rs.next()) {
+                    actualDatabaseName = rs.getString(1);
+                }
+            }
+            // Check if the driver reconnected to the expected database.
+            assertEquals(expectedDatabaseName, actualDatabaseName);
+        } finally {
+            TestUtils.dropDatabaseIfExists(expectedDatabaseName, connectionString);
+        }
+    }
+
+    @Test
+    public void testSetLanguage() throws SQLException {
+        String expectedLanguage = "Italiano";
+        String actualLanguage = "";
+        try (Connection c = ResiliencyUtils.getConnection(connectionString); Statement s = c.createStatement()) {
+            s.execute("SET LANGUAGE " + expectedLanguage);
+            ResiliencyUtils.killConnection(c, connectionString);
+            try (ResultSet rs = s.executeQuery("SELECT @@LANGUAGE")) {
+                while (rs.next()) {
+                    actualLanguage = rs.getString(1);
+                }
+            }
+            // Check if the driver reconnected to the expected database.
+            assertEquals(expectedLanguage, actualLanguage);
+        }
+    }
+
+    @Test
+    public void testOpenTransaction() throws SQLException {
+        String tableName = RandomUtil.getIdentifier("resTable");
+        try (Connection c = ResiliencyUtils.getConnection(connectionString); Statement s = c.createStatement()) {
+            TestUtils.dropTableIfExists(tableName, s);
+            s.execute("CREATE TABLE [" + tableName + "] (col1 varchar(1))");
+            c.setAutoCommit(false);
+            s.execute("INSERT INTO [" + tableName + "] values ('x')");
+            ResiliencyUtils.killConnection(c, connectionString);
+            try (ResultSet rs = s.executeQuery("SELECT db_name();")) {
+                fail("Connection resiliency should not have reconnected with an open transaction!");
+            } catch (SQLException ex) {
+                assertTrue(ex.getMessage().matches(TestUtils.formatErrorMsg("R_crServerSessionStateNotRecoverable")));
+            }
+        }
+        try (Connection c = DriverManager.getConnection(connectionString); Statement s = c.createStatement()) {
+            TestUtils.dropTableIfExists(tableName, s);
+        }
+    }
+
+    @Test
+    public void testOpenResultSetShouldBeCleanedUp() throws SQLException, InterruptedException {
+        try (Connection c = ResiliencyUtils.getConnection(connectionString); Statement s = c.createStatement()) {
+            int sessionId = ResiliencyUtils.getSessionId(c);
+            try (ResultSet rs = s.executeQuery("select 2")) {
+                rs.next();
+                ResiliencyUtils.killConnection(sessionId, connectionString, c);
+            } catch (SQLException ex) {
+                ex.printStackTrace();
+                fail("Connection failed to clean up open resultset.");
+            }
+        }
+    }
+
+    @Test
+    public void testPooledConnection() throws SQLException {
+        SQLServerConnectionPoolDataSource mds = new SQLServerConnectionPoolDataSource();
+        mds.setURL(connectionString);
+        PooledConnection pooledConnection = mds.getPooledConnection();
+        try (Connection c = pooledConnection.getConnection(); Statement s = c.createStatement()) {
+            ResiliencyUtils.minimizeIdleNetworkTracker(c);
+            c.close();
+            Connection c1 = pooledConnection.getConnection();
+            Statement s1 = c1.createStatement();
+            ResiliencyUtils.killConnection(c1, connectionString);
+            ResiliencyUtils.minimizeIdleNetworkTracker(c1);
+            s1.executeQuery("SELECT 1");
+        } catch (SQLException e) {
+            fail(e.getMessage());
+        }
+    }
+
+    @Test
+    @Tag(Constants.xAzureSQLDB) // Switching databases is not supported against Azure, skip/
+    @Tag(Constants.xAzureSQLDW)
+    public void testPooledConnectionDB() throws SQLException {
+        SQLServerConnectionPoolDataSource mds = new SQLServerConnectionPoolDataSource();
+        mds.setURL(connectionString);
+        PooledConnection pooledConnection = mds.getPooledConnection();
+        String newDBName = null;
+        String resultDBName = null;
+        String originalDBName = null;
+        try (Connection c = pooledConnection.getConnection(); Statement s = c.createStatement()) {
+            ResiliencyUtils.minimizeIdleNetworkTracker(c);
+            ResultSet rs = s.executeQuery("SELECT DB_NAME();");
+            rs.next();
+            originalDBName = rs.getString(1);
+            newDBName = RandomUtil.getIdentifier("resDB");
+            TestUtils.dropDatabaseIfExists(newDBName, connectionString);
+            s.execute("CREATE DATABASE [" + newDBName + "]");
+            try {
+                s.execute("USE [" + newDBName + "]");
+            } catch (SQLException e) {
+                return;
+            }
+            c.close();
+            Connection c1 = pooledConnection.getConnection();
+            Statement s1 = c1.createStatement();
+            ResiliencyUtils.killConnection(c1, connectionString);
+            ResiliencyUtils.minimizeIdleNetworkTracker(c1);
+            rs = s1.executeQuery("SELECT db_name();");
+            while (rs.next()) {
+                resultDBName = rs.getString(1);
+            }
+            // Check if the driver reconnected to the expected database.
+            assertEquals(originalDBName, resultDBName);
+        } finally {
+            TestUtils.dropDatabaseIfExists(newDBName, connectionString);
+        }
+    }
+
+    @Test
+    public void testPooledConnectionLang() throws SQLException {
+        SQLServerConnectionPoolDataSource mds = new SQLServerConnectionPoolDataSource();
+        mds.setURL(connectionString);
+        PooledConnection pooledConnection = mds.getPooledConnection();
+        String lang0 = null, lang1 = null;
+
+        try (Connection c = pooledConnection.getConnection(); Statement s = c.createStatement()) {
+            ResiliencyUtils.minimizeIdleNetworkTracker(c);
+            ResultSet rs = s.executeQuery("SELECT @@LANGUAGE;");
+            while (rs.next())
+                lang0 = rs.getString(1);
+            s.execute("SET LANGUAGE FRENCH;");
+            c.close();
+            try (Connection c1 = pooledConnection.getConnection(); Statement s1 = c1.createStatement()) {
+                ResiliencyUtils.killConnection(c1, connectionString);
+                ResiliencyUtils.minimizeIdleNetworkTracker(c1);
+                rs = s1.executeQuery("SELECT @@LANGUAGE;");
+                while (rs.next())
+                    lang1 = rs.getString(1);
+                assertEquals(lang0, lang1);
+            } finally {
+                rs.close();
+            }
+        } catch (SQLException e) {
+            e.printStackTrace();
+            fail(e.toString());
+        }
+    }
+
+    private void basicReconnect(String connectionString) throws SQLException {
+        try (Connection c = ResiliencyUtils.getConnection(connectionString)) {
+            try (Statement s = c.createStatement()) {
+                ResiliencyUtils.killConnection(c, connectionString);
+                s.executeQuery("SELECT 1");
+            }
+        }
+    }
+}
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/PropertyTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/PropertyTest.java
new file mode 100644
index 00000000..b62f768b
--- /dev/null
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/PropertyTest.java
@@ -0,0 +1,94 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+
+package com.microsoft.sqlserver.jdbc.resiliency;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.concurrent.ThreadLocalRandom;
+
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import com.microsoft.sqlserver.jdbc.TestResource;
+import com.microsoft.sqlserver.jdbc.TestUtils;
+import com.microsoft.sqlserver.testframework.AbstractTest;
+import com.microsoft.sqlserver.testframework.Constants;
+
+
+@Tag(Constants.xSQLv11)
+public class PropertyTest extends AbstractTest {
+
+    private void testInvalidPropertyOverBrokenConnection(String prop, String val,
+            String expectedErrMsg) throws SQLException {
+        StringBuilder sb = new StringBuilder();
+        sb.append(connectionString).append(";").append(prop).append("=").append(val).append(";");
+        try (Connection c = ResiliencyUtils.getConnection(sb.toString())) {
+            try (Statement s = c.createStatement()) {
+                ResiliencyUtils.killConnection(c, connectionString);
+                s.executeQuery("SELECT 1");
+                fail(TestResource.getResource("R_expectedExceptionNotThrown") + prop + "=" + val);
+            }
+        } catch (SQLException e) {
+            assertTrue(TestResource.getResource("R_unexpectedErrorMessage") + e.getMessage(), e.getMessage().matches(expectedErrMsg));
+        }
+    }
+
+    @Test
+    public void testRetryCount() throws SQLException {
+        // fail immediately without retrying
+        testInvalidPropertyOverBrokenConnection("connectRetryCount", "0", ".*");
+        // Out of range, < 0
+        testInvalidPropertyOverBrokenConnection("connectRetryCount",
+                String.valueOf(ThreadLocalRandom.current().nextInt(Integer.MIN_VALUE, 0)),
+                TestUtils.formatErrorMsg("R_invalidConnectRetryCount"));
+        // Out of range, > 255
+        testInvalidPropertyOverBrokenConnection("connectRetryCount",
+                String.valueOf(ThreadLocalRandom.current().nextInt(256, Integer.MAX_VALUE)),
+                TestUtils.formatErrorMsg("R_invalidConnectRetryCount"));
+        // non-Integer types: boolean, float, double, string
+        testInvalidPropertyOverBrokenConnection("connectRetryCount",
+                String.valueOf(ThreadLocalRandom.current().nextBoolean()),
+                TestUtils.formatErrorMsg("R_invalidConnectRetryCount"));
+        testInvalidPropertyOverBrokenConnection("connectRetryCount",
+                String.valueOf(ThreadLocalRandom.current().nextFloat()),
+                TestUtils.formatErrorMsg("R_invalidConnectRetryCount"));
+        testInvalidPropertyOverBrokenConnection("connectRetryCount",
+                String.valueOf(ThreadLocalRandom.current().nextDouble()),
+                TestUtils.formatErrorMsg("R_invalidConnectRetryCount"));
+        testInvalidPropertyOverBrokenConnection("connectRetryCount",
+                ResiliencyUtils.getRandomString(ResiliencyUtils.alpha, 15),
+                TestUtils.formatErrorMsg("R_invalidConnectRetryCount"));
+    }
+
+    @Test
+    public void testRetryInterval() throws SQLException {
+        // Out of range, < 1
+        testInvalidPropertyOverBrokenConnection("connectRetryInterval",
+                String.valueOf(ThreadLocalRandom.current().nextInt(Integer.MIN_VALUE, 1)),
+                TestUtils.formatErrorMsg("R_invalidConnectRetryInterval"));
+        // Out of range, > 60
+        testInvalidPropertyOverBrokenConnection("connectRetryInterval",
+                String.valueOf(ThreadLocalRandom.current().nextInt(61, Integer.MAX_VALUE)),
+                TestUtils.formatErrorMsg("R_invalidConnectRetryInterval"));
+        // non-Integer types: boolean, float, double, string
+        testInvalidPropertyOverBrokenConnection("connectRetryInterval",
+                String.valueOf(ThreadLocalRandom.current().nextBoolean()),
+                TestUtils.formatErrorMsg("R_invalidConnectRetryInterval"));
+        testInvalidPropertyOverBrokenConnection("connectRetryInterval",
+                String.valueOf(ThreadLocalRandom.current().nextFloat()),
+                TestUtils.formatErrorMsg("R_invalidConnectRetryInterval"));
+        testInvalidPropertyOverBrokenConnection("connectRetryInterval",
+                String.valueOf(ThreadLocalRandom.current().nextDouble()),
+                TestUtils.formatErrorMsg("R_invalidConnectRetryInterval"));
+        testInvalidPropertyOverBrokenConnection("connectRetryInterval",
+                ResiliencyUtils.getRandomString(ResiliencyUtils.alpha, 15),
+                TestUtils.formatErrorMsg("R_invalidConnectRetryInterval"));
+    }
+}
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/ReflectiveTests.java b/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/ReflectiveTests.java
new file mode 100644
index 00000000..49338c48
--- /dev/null
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/ReflectiveTests.java
@@ -0,0 +1,164 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+
+package com.microsoft.sqlserver.jdbc.resiliency;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import com.microsoft.sqlserver.jdbc.TestResource;
+import com.microsoft.sqlserver.jdbc.TestUtils;
+import com.microsoft.sqlserver.testframework.AbstractTest;
+import com.microsoft.sqlserver.testframework.Constants;
+
+
+@Tag(Constants.xSQLv11)
+public class ReflectiveTests extends AbstractTest {
+
+    private void timeoutVariations(Map<String, String> props, long expectedDuration,
+            Optional<String> expectedErrMsg) throws SQLException {
+        long startTime = 0;
+        String cs = ResiliencyUtils.setConnectionProps(connectionString.concat(";"), props);
+        try (Connection c = ResiliencyUtils.getConnection(cs)) {
+            try (Statement s = c.createStatement()) {
+                ResiliencyUtils.killConnection(c, connectionString);
+                ResiliencyUtils.blockConnection(c);
+                startTime = System.currentTimeMillis();
+                s.executeQuery("SELECT 1");
+                fail("Successfully executed query on a blocked connection.");
+            } catch (SQLException e) {
+                double elapsedTime = System.currentTimeMillis() - startTime;
+                // Timeout should occur after query timeout and not login timeout
+                assertTrue("Query did not timeout in " + expectedDuration + "ms, elapsed time(ms): " + elapsedTime,
+                        elapsedTime < expectedDuration);
+                if (expectedErrMsg.isPresent()) {
+                    assertTrue(TestResource.getResource("R_unexpectedErrorMessage") + e.getMessage(),
+                            e.getMessage().matches(TestUtils.formatErrorMsg(expectedErrMsg.get())));
+                }
+            }
+        }
+    }
+
+    /*
+     * Command with default ReconnectRetryDelay is executed over a broken connection Expected: Client waits exactly 10
+     * seconds between attempts Expected to timeout in 20 seconds, (retryDelay * (retryCount-1)) +
+     * (loginTimeout*retryCount)
+     */
+    @Test
+    public void testDefaultTimeout() throws SQLException {
+        Map<String, String> m = new HashMap<>();
+        m.put("connectRetryCount", "2");
+        m.put("loginTimeout", "5");
+        timeoutVariations(m, 25000, Optional.empty());
+    }
+
+    /*
+     * Default retry count is 1. Expect timeout to be just above login timeout.
+     */
+    @Test
+    public void testDefaultRetry() throws SQLException {
+        Map<String, String> m = new HashMap<>();
+        m.put("loginTimeout", "5");
+        timeoutVariations(m, 6000, Optional.empty());
+    }
+
+    /*
+     * Command with ReconnectRetryDelay > QueryTimeout is executed over a broken connection Expected: Client reports
+     * query timeout immediately without waiting for ReconenctRetryDelay
+     */
+    @Test
+    public void testReconnectDelayQueryTimeout() throws SQLException {
+        Map<String, String> m = new HashMap<>();
+        m.put("connectRetryCount", "3");
+        m.put("connectRetryInterval", "30");
+        m.put("queryTimeout", "5");
+        timeoutVariations(m, 10000, Optional.empty());
+    }
+
+    /*
+     * Command with infinite ConnectionTimeout and ReconnectRetryCount == 1 is executed over a broken connection
+     * Expected: Client times out by QueryTimeout
+     */
+    @Test
+    public void testQueryTimeout() throws SQLException {
+        Map<String, String> m = new HashMap<>();
+        m.put("queryTimeout", "10");
+        m.put("loginTimeout", "65535");
+        m.put("connectRetryCount", "1");
+        timeoutVariations(m, 12000, Optional.empty());
+    }
+
+    /*
+     * Command with QueryTimeout > (ReconnectRetryCount * (ReconnectRetryDelay + ConnectionTimeout)) is executed over a
+     * broken connection Expected: Client fails after ReconnectRetryCount attempts
+     */
+    @Test
+    public void testValidRetryWindow() throws SQLException {
+        Map<String, String> m = new HashMap<>();
+        m.put("queryTimeout", "-1");
+        m.put("loginTimeout", "5");
+        m.put("connectRetryCount", "2");
+        m.put("connectRetryInterval", "10");
+        timeoutVariations(m, 25000, Optional.of("R_crClientAllRecoveryAttemptsFailed"));
+    }
+
+    @Test
+    public void testRequestRecovery() throws SQLException, IllegalArgumentException, IllegalAccessException, NoSuchMethodException, SecurityException, InvocationTargetException {
+        Map<String, String> m = new HashMap<>();
+        m.put("connectRetryCount", "1");
+        String cs = ResiliencyUtils.setConnectionProps(connectionString.concat(";"), m);
+
+        try (Connection c = DriverManager.getConnection(cs)) {
+            Field fields[] = c.getClass().getSuperclass().getDeclaredFields();
+            for (Field f : fields) {
+                if (f.getName() == "sessionRecovery") {
+                    f.setAccessible(true);
+                    Object sessionRecoveryFeature = f.get(c);
+                    Method method = sessionRecoveryFeature.getClass()
+                            .getDeclaredMethod("isConnectionRecoveryNegotiated");
+                    method.setAccessible(true);
+                    boolean b = (boolean) method.invoke(sessionRecoveryFeature);
+                    assertTrue("Session Recovery not negotiated when requested", b);
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testNoRecovery() throws SQLException, IllegalArgumentException, IllegalAccessException, NoSuchMethodException, SecurityException, InvocationTargetException {
+        Map<String, String> m = new HashMap<>();
+        m.put("connectRetryCount", "0");
+        String cs = ResiliencyUtils.setConnectionProps(connectionString.concat(";"), m);
+
+        try (Connection c = DriverManager.getConnection(cs)) {
+            Field fields[] = c.getClass().getSuperclass().getDeclaredFields();
+            for (Field f : fields) {
+                if (f.getName() == "sessionRecovery") {
+                    f.setAccessible(true);
+                    Object sessionRecoveryFeature = f.get(c);
+                    Method method = sessionRecoveryFeature.getClass()
+                            .getDeclaredMethod("isConnectionRecoveryNegotiated");
+                    method.setAccessible(true);
+                    assertTrue("Session Recovery received when not negotiated",
+                            !(boolean) method.invoke(sessionRecoveryFeature));
+                }
+            }
+        }
+    }
+}
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/ResiliencyUtils.java b/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/ResiliencyUtils.java
new file mode 100644
index 00000000..bd2f8445
--- /dev/null
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/ResiliencyUtils.java
@@ -0,0 +1,393 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+
+package com.microsoft.sqlserver.jdbc.resiliency;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+
+import javax.sql.PooledConnection;
+
+import org.junit.Assert;
+import org.junit.jupiter.api.Tag;
+
+import com.microsoft.sqlserver.jdbc.SQLServerConnectionPoolDataSource;
+import com.microsoft.sqlserver.jdbc.TestUtils;
+import com.microsoft.sqlserver.testframework.Constants;
+
+
+@Tag(Constants.xSQLv11)
+final class ResiliencyUtils {
+
+    private static final String[] ON_OFF = new String[] {"ON", "OFF"};
+    static final String alpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+    static final int checkRecoveryAliveInterval = 500;
+
+    private ResiliencyUtils() {};
+
+    enum USER_OPTIONS {
+        ANSI_DEFAULTS(ON_OFF),
+        ANSI_NULL_DFLT_OFF(ON_OFF),
+        ANSI_NULLS(ON_OFF),
+        ANSI_PADDING(ON_OFF),
+        ANSI_WARNINGS(ON_OFF),
+        ARITHABORT(ON_OFF),
+        CONCAT_NULL_YIELDS_NULL(ON_OFF),
+        // CONTEXT_INFO,
+        CURSOR_CLOSE_ON_COMMIT(ON_OFF),
+        DATEFIRST(new String[] {"1", "2", "3", "4", "5", "6", "7"}),
+        DATEFORMAT(new String[] {"dmy", "dym", "mdy", "myd", "ydm", "ymd"}),
+        DEADLOCK_PRIORITY(new String[] {"LOW", "NORMAL", "HIGH", "-10", "-9", "-8", "-7", "-6", "-5", "-4", "-3", "-2",
+                "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}),
+        // FIPS_FLAGGER(new String[] {"ENTRY", "FULL", "INTERMEDIATE", "OFF"}), not picked up by dbcc useroptions
+        // FMTONLY fails queries
+        FORCEPLAN(ON_OFF),
+        // IDENTITY_INSERT(ON_OFF),
+        IMPLICIT_TRANSACTIONS(ON_OFF),
+        LANGUAGE(new String[] {"English", "German", "French", "Japanese", "Danish", "Spanish", "Italian", "Dutch",
+                "Norwegian", "Portuguese", "Finnish", "Swedish", "Czech", "Hungarian", "Polish", "Romanian", "Croatian",
+                "Slovak", "Slovenian", "Greek", "Bulgarian", "Russian", "Turkish", "British English", "Estonian",
+                "Latvian", "Lithuanian", "Brazilian", "Traditional Chinese", "Korean", "Simplified Chinese", "Arabic",
+                "Thai"}) {
+            @Override
+            String getValue() {
+                return "N'" + value + "'";
+            }
+        },
+        LOCK_TIMEOUT(null) {
+            @Override
+            void init() {
+                this.value = String.valueOf(getRandomInt(0, 8000));
+            }
+        },
+        NOCOUNT(ON_OFF),
+        // NOEXEC fails queries
+        NUMERIC_ROUNDABORT(ON_OFF),
+        // PARSEONLY fails queries
+        QUERY_GOVERNOR_COST_LIMIT(null) {
+            @Override
+            void init() {
+                this.value = String.valueOf(getRandomInt(0, 8000));
+            }
+        },
+        QUOTED_IDENTIFIER(ON_OFF),
+        ROWCOUNT(null) {
+            @Override
+            void init() {
+                this.value = String.valueOf(getRandomInt(0, 8000));
+            }
+        },
+        // SHOWPLAN_ALL/SHOWPLAN_TEXT/SHOWPLAN_XML fails queries
+        STATISTICS_IO(ON_OFF) {
+            @Override
+            public String toString() {
+                return "STATISTICS IO";
+            }
+        },
+        STATISTICS_PROFILE(ON_OFF) {
+            @Override
+            public String toString() {
+                return "STATISTICS PROFILE";
+            }
+        },
+        STATISTICS_TIME(ON_OFF) {
+            @Override
+            public String toString() {
+                return "STATISTICS TIME";
+            }
+        },
+        STATISTICS_XML(ON_OFF) {
+            @Override
+            public String toString() {
+                return "STATISTICS XML";
+            }
+        },
+        TEXTSIZE(null) {
+            @Override
+            void init() {
+                this.value = String.valueOf(getRandomInt(0, 8000));
+            }
+        },
+        TRANSACTION_ISOLATION_LEVEL(new String[] {"READ UNCOMMITTED", "READ COMMITTED", "REPEATABLE READ", "SNAPSHOT",
+                "SERIALIZABLE"}) {
+            @Override
+            public String toString() {
+                return "TRANSACTION ISOLATION LEVEL";
+            }
+        },
+        XACT_ABORT(ON_OFF);
+
+        private String[] possibleOptions;
+        String value;
+
+        private USER_OPTIONS(String[] s) {
+            this.possibleOptions = s;
+            init();
+        }
+
+        void init() {
+            this.value = possibleOptions[getRandomInt(0, possibleOptions.length)];
+        }
+
+        String getValue() {
+            return value;
+        }
+    }
+
+    class killConnectionThread implements Runnable {
+
+        Connection c;
+        String cString;
+        int sleepTime = 0;
+
+        public void run() {
+            try {
+                int sessionID = 0;
+                try (Statement s = c.createStatement()) {
+                    try (ResultSet rs = s.executeQuery("SELECT @@SPID")) {
+                        while (rs.next()) {
+                            sessionID = rs.getInt(1);
+                        }
+                    }
+                }
+                try (Connection c2 = DriverManager.getConnection(cString)) {
+                    try (Statement s = c2.createStatement()) {
+                        Thread.sleep(sleepTime);
+                        s.execute("KILL " + sessionID);
+                    }
+                }
+            } catch (SQLException | InterruptedException e) {
+                // handle exception
+            }
+        }
+
+        protected void setProperties(Connection c, String cString) {
+            this.c = c;
+            this.cString = cString;
+        }
+
+        public void setTimer(int millis) {
+            this.sleepTime = millis;
+        }
+    }
+
+    static Connection getPooledConnection(String connectionString) throws SQLException {
+        SQLServerConnectionPoolDataSource mds = new SQLServerConnectionPoolDataSource();
+        mds.setURL(connectionString);
+        PooledConnection pooledConnection = mds.getPooledConnection();
+        Connection c = pooledConnection.getConnection();
+
+        minimizeIdleNetworkTracker(c);
+        return c;
+    }
+
+    static Connection getConnection(String connectionString) throws SQLException {
+        Connection c = DriverManager.getConnection(connectionString);
+        minimizeIdleNetworkTracker(c);
+        return c;
+    }
+
+    static void minimizeIdleNetworkTracker(Connection c) {
+        try {
+            Connection conn = c;
+
+            // See if we were handed a pooled connection
+            for (Field f : c.getClass().getDeclaredFields()) {
+                if (f.getName() == "wrappedConnection") {
+                    f.setAccessible(true);
+                    conn = (Connection) f.get(c);
+                    break;
+                }
+            }
+
+            boolean methodInvoked = false;
+            for (Field f : getConnectionFields(conn)) {
+                if (f.getName() == "idleNetworkTracker") {
+                    f.setAccessible(true);
+                    Object idleNetworkTracker = f.get(conn);
+                    Method method = idleNetworkTracker.getClass().getDeclaredMethod("setMaxIdleMillis", int.class);
+                    method.setAccessible(true);
+                    method.invoke(idleNetworkTracker, -1);
+                    methodInvoked = true;
+                    break;
+                }
+            }
+            if (!methodInvoked) {
+                throw new Exception("Failed to find setMaxIdleMillis via reflection to adjust the internal idle time.");
+            }
+        } catch (Exception e) {
+            Assert.fail("Failed to setMaxIdleMillis in Connection's idleNetworkTracker: " + e.getMessage());
+        }
+    }
+
+    /**
+     * Since we are using reflection to the IdleNetworkTracker to immediately deem a connection idle, in certain
+     * scenarios (ie testing against Azure SQL DB), it takes too long for the connection to drop from the server side
+     * after sending a kill connection command. This method checks to make sure the recovery thread exists and the
+     * connection has died before proceeding.
+     * 
+     * @param c
+     *        Connection
+     * @return True if recovery thread was started and connection is dead, false if timeout
+     */
+    static boolean isRecoveryAliveAndConnDead(Connection c) {
+        Connection conn = c;
+        // See if we were handed a pooled connection
+        for (Field f : getConnectionFields(c)) {
+            if (f.getName() == "wrappedConnection") {
+                f.setAccessible(true);
+                try {
+                    conn = (Connection) f.get(c);
+                } catch (IllegalArgumentException | IllegalAccessException e) {
+                    e.printStackTrace();
+                }
+                break;
+            }
+        }
+
+        int waits = 0;
+        try {
+            while (!TestUtils.isConnectionRecoveryNegotiated(conn)) {
+                TimeUnit.MILLISECONDS.sleep(ResiliencyUtils.checkRecoveryAliveInterval);
+                if (waits++ > 5)
+                    return false;
+            }
+            while (!TestUtils.isConnectionDead(conn)) {
+                TimeUnit.MILLISECONDS.sleep(ResiliencyUtils.checkRecoveryAliveInterval);
+                if (waits++ > 5)
+                    return false;
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return true;
+    }
+
+    static int getSessionId(Connection c) throws SQLException {
+        int sessionID = 0;
+        try (Statement s = c.createStatement()) {
+            try (ResultSet rs = s.executeQuery("SELECT @@SPID")) {
+                while (rs.next()) {
+                    sessionID = rs.getInt(1);
+                }
+            }
+        }
+        return sessionID;
+    }
+
+    static void killConnection(Connection c, String cString) throws SQLException {
+        killConnection(getSessionId(c), cString, c);
+    }
+
+    /**
+     * Running a query on a connection can affect it adversely, this method should not run a query on the connection
+     * being passed.
+     * 
+     * @param sessionID
+     * @param cString
+     * @param c
+     * @throws SQLException
+     */
+    static void killConnection(int sessionID, String cString, Connection c) throws SQLException {
+        try (Connection c2 = DriverManager.getConnection(cString)) {
+            try (Statement s = c2.createStatement()) {
+                if (TestUtils.isAzureDW(c2)) // AzureSQLDW and Synapse uses different syntax
+                    s.execute("KILL '" + sessionID + "'");
+                else
+                    s.execute("KILL " + sessionID);
+            }
+        }
+        isRecoveryAliveAndConnDead(c);
+    }
+
+    // uses reflection to "corrupt" a Connection's server target
+    static void blockConnection(Connection c) throws SQLException {
+        Field fields[] = getConnectionFields(c);
+        for (Field f : fields) {
+            if (f.getName() == "activeConnectionProperties" && Properties.class == f.getType()) {
+                f.setAccessible(true);
+                Properties connectionProps;
+                try {
+                    connectionProps = (Properties) f.get(c);
+                    connectionProps.setProperty("serverName", "bogusServerName");
+                    f.set(c, connectionProps);
+                    return;
+                } catch (IllegalArgumentException | IllegalAccessException e) {
+                    Assert.fail("Failed to block connection: " + e.getMessage());
+                }
+            }
+        }
+        Assert.fail("Failed to block connection.");
+    }
+
+    static Map<String, String> getUserOptions(Connection c) throws SQLException {
+        Map<String, String> options = new HashMap<>();
+        try (Statement stmt = c.createStatement()) {
+            try (ResultSet rs = stmt.executeQuery("DBCC USEROPTIONS")) {
+                while (rs.next()) {
+                    String key = rs.getString(1);
+                    String value = rs.getObject(2).toString();
+                    options.put(key, value);
+                }
+            }
+        }
+        return options;
+    }
+
+    static void toggleRandomProperties(Connection c) throws SQLException {
+        try (Statement stmt = c.createStatement()) {
+            for (USER_OPTIONS uo : USER_OPTIONS.values()) {
+                stmt.execute("SET " + uo.toString() + " " + uo.getValue());
+            }
+        }
+    }
+
+    static int getRandomInt(int min, int max) {
+        return ThreadLocalRandom.current().nextInt(min, max);
+    }
+
+    static String getRandomString(String pool, int length) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < length; i++) {
+            sb.append(String.valueOf(pool.charAt(getRandomInt(0, pool.length()))));
+        }
+        return sb.toString();
+    }
+
+    static String setConnectionProps(String base, Map<String, String> props) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(base);
+        props.forEach((k, v) -> sb.append(k).append("=").append(v).append(";"));
+        return sb.toString();
+    }
+
+    /**
+     * Get declared fields of connection class depending on Java version. Connection class SQLServerConnection43 is
+     * returned for Java >=9 and SQLServerConnection or SQLServerConnectPoolProxy for Java 8
+     * 
+     * @param c
+     *        connection class that implements ISQLServerConnection
+     * @return declared fields for Connection class
+     */
+    static Field[] getConnectionFields(Connection c) {
+        Class<? extends Connection> cls = c.getClass();
+        // SQLServerConnection43 is returned for Java >=9 so need to get super class
+        if (cls.getName() == "com.microsoft.sqlserver.jdbc.SQLServerConnection43") {
+            return cls.getSuperclass().getDeclaredFields();
+        }
+
+        return cls.getDeclaredFields();
+    }
+}
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/ResultSetsWithResiliencyTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/ResultSetsWithResiliencyTest.java
new file mode 100644
index 00000000..ca797414
--- /dev/null
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/ResultSetsWithResiliencyTest.java
@@ -0,0 +1,166 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+
+package com.microsoft.sqlserver.jdbc.resiliency;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import com.microsoft.sqlserver.jdbc.RandomUtil;
+import com.microsoft.sqlserver.jdbc.SQLServerException;
+import com.microsoft.sqlserver.jdbc.TestUtils;
+import com.microsoft.sqlserver.testframework.AbstractTest;
+import com.microsoft.sqlserver.testframework.Constants;
+
+
+@Tag(Constants.xSQLv11)
+public class ResultSetsWithResiliencyTest extends AbstractTest {
+    static String tableName = "[" + RandomUtil.getIdentifier("resTable") + "]";
+    static int numberOfRows = 10;
+
+    @BeforeAll
+    public static void setUp() throws SQLException {
+        try (Connection c = DriverManager.getConnection(connectionString); Statement s = c.createStatement();) {
+            TestUtils.dropTableIfExists(tableName, s);
+            createTable(s);
+            insertData(s);
+        }
+    }
+
+    /*
+     * Execute statement with adaptive buffering on a broken connection.
+     */
+    @Test
+    public void testAdaptiveBuffering() throws SQLException, InterruptedException {
+        verifyResultSetResponseBuffering("adaptive", true);
+    }
+
+    /*
+     * Execute statement with full buffering on a broken connection.
+     */
+    @Test
+    public void testFullBuffering() throws SQLException, InterruptedException {
+        verifyResultSetResponseBuffering("full", true);
+    }
+
+    /*
+     * Execute statement with adaptive buffering and no strong reference to result set.
+     */
+    @Test
+    public void testAdaptiveBufferingNoStrongReferenceToResultSet() throws SQLException, InterruptedException {
+        verifyResultSetResponseBuffering("adaptive", false);
+    }
+
+    /*
+     * Execute statement with full buffering and no strong reference to result set.
+     */
+    @Test
+    public void testFullBufferingNoStrongReferenceToResultSet() throws SQLException, InterruptedException {
+        verifyResultSetResponseBuffering("full", false);
+    }
+
+    /*
+     * Execute statement when previous result set completely buffered but partially parsed by application.
+     */
+    @Test
+    public void testFullBufferingWithPartiallyParsedResultSet() throws SQLException {
+        try (Connection c = ResiliencyUtils.getConnection(connectionString + ";responseBuffering=full");
+                Statement s = c.createStatement(); Statement s2 = c.createStatement()) {
+            int sessionId = ResiliencyUtils.getSessionId(c);
+            try (ResultSet rs = s.executeQuery("SELECT * FROM sys.syslanguages")) {
+                // Partially parsed
+                rs.next();
+                rs.getString(2);
+                ResiliencyUtils.killConnection(sessionId, connectionString, c);
+                // ResultSet is not completely parsed, connection recovery is disabled.
+                s2.execute("SELECT 1");
+                fail("Driver should not have succesfully reconnected but it did.");
+            }
+        } catch (SQLServerException e) {
+            if (!e.getMessage().matches(TestUtils.formatErrorMsg("R_crClientUnrecoverable"))) {
+                e.printStackTrace();
+            }
+            assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_crClientUnrecoverable")));
+            
+        }
+    }
+
+    /*
+     * Execute statement when previous result set partially buffered 
+     */
+    @Test
+    public void testAdaptiveBufferingWithPartiallyBufferedResultSet() throws SQLException {
+        // The table must contain enough rows to partially buffer the result set.
+        try (Connection c = ResiliencyUtils.getConnection(connectionString + ";responseBuffering=adaptive");
+                Statement s = c.createStatement(); Statement s2 = c.createStatement()) {
+            int sessionId = ResiliencyUtils.getSessionId(c);
+            try (ResultSet rs = s.executeQuery("SELECT * FROM " + tableName + " ORDER BY id;")) {
+                ResiliencyUtils.killConnection(sessionId, connectionString, c);
+                // ResultSet is partially buffered, connection recovery is disabled.
+                s2.execute("SELECT 1");
+                fail("Driver should not have succesfully reconnected but it did.");
+            }
+        } catch (SQLServerException e) {
+            assertTrue("08S01" == e.getSQLState()
+                    || e.getMessage().matches(TestUtils.formatErrorMsg("R_crClientUnrecoverable")));
+            
+        }
+    }
+
+    private static void createTable(Statement s) throws SQLException {
+        s.execute("CREATE TABLE " + tableName + " (id int IDENTITY, data varchar(50));");
+    }
+
+    private static void insertData(Statement s) throws SQLException {
+        for (int i = 1; i <= numberOfRows; i++) {
+            s.executeUpdate("INSERT INTO " + tableName + " VALUES ('testData" + i + "');");
+        }
+    }
+
+    private void verifyResultSet(ResultSet rs) throws SQLException {
+        int count = 0;
+        while (rs.next()) {
+            count++;
+            if (!("testData" + count).equals(rs.getString(2))) {
+                fail();
+            }
+        }
+        assertEquals(numberOfRows, count);
+    }
+
+    private void verifyResultSetResponseBuffering(String responseBuffering,
+            boolean strongReferenceToResultSet) throws SQLException, InterruptedException {
+        try (Connection c = ResiliencyUtils.getConnection(connectionString + ";responseBuffering=" + responseBuffering);
+                Statement s = c.createStatement()) {
+            ResiliencyUtils.killConnection(c, connectionString);
+            if (strongReferenceToResultSet) {
+                try (ResultSet rs = s.executeQuery("SELECT * FROM " + tableName + " ORDER BY id;")) {
+                    verifyResultSet(rs);
+                }
+            } else {
+                s.executeQuery("SELECT * FROM " + tableName + " ORDER BY id;");
+            }
+        }
+    }
+
+    @AfterAll
+    public static void cleanUp() throws SQLException {
+        try (Connection c = DriverManager.getConnection(connectionString); Statement s = c.createStatement()) {
+            TestUtils.dropTableIfExists(tableName, s);
+        }
+    }
+}
diff --git a/src/test/java/com/microsoft/sqlserver/testframework/Constants.java b/src/test/java/com/microsoft/sqlserver/testframework/Constants.java
index e7060e5b..acac806c 100644
--- a/src/test/java/com/microsoft/sqlserver/testframework/Constants.java
+++ b/src/test/java/com/microsoft/sqlserver/testframework/Constants.java
@@ -16,6 +16,7 @@ public final class Constants {
      * <pre>
      * xJDBC42 - - - - - - For tests not compatible with JDBC 42 Specifications
      * xGradle - - - - - - For tests not compatible with Gradle Script (e.g. Manifest File)
+     * xSQLv11 - - - - - - For tests not compatible with SQL Server 2012
      * xSQLv12 - - - - - - For tests not compatible with SQL Server 2008 R2 - 2014
      * xSQLv14 - - - - - - For tests not compatible with SQL Server 2016 - 2017
      * xSQLv15 - - - - - - For tests not compatible with SQL Server 2019
@@ -30,6 +31,7 @@ public final class Constants {
      */
     public static final String xJDBC42 = "xJDBC42";
     public static final String xGradle = "xGradle";
+    public static final String xSQLv11 = "xSQLv11";
     public static final String xSQLv12 = "xSQLv12";
     public static final String xSQLv14 = "xSQLv14";
     public static final String xSQLv15 = "xSQLv15";