Repository: microsoft/mssql-jdbc
Pull request #2324 (2024-02-08 20:57:03)

Add support for TDSType.GUID (#1582)
This PR intends to provide support for sending UUID parameters as `TDSType.GUID (0x24)` on the wire and (hopefully) implements feature request #1582. 
It currently requires the Java parameter be given to the driver as `String` with `JDBCType.GUID`, i.e. :
```
st.setObject(index, myUUID.toString(), microsoft.sql.Types.GUID);
```


microsoft-github-policy-service[bot]: 
@vxel please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

```
@microsoft-github-policy-service agree [company="{your company}"]
```

> Options:
> - (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
>```
>@microsoft-github-policy-service agree
>```
> - (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
>```
>@microsoft-github-policy-service agree company="Microsoft"
>```

<details>
    <summary>Contributor License Agreement</summary>
    
### Contribution License Agreement
This Contribution License Agreement (**“Agreement”**) is agreed to by the party signing below (**“You”**),
and conveys certain license rights to Microsoft Corporation and its affiliates (“Microsoft”) for Your
contributions to Microsoft open source projects. This Agreement is effective as of the latest signature
date below.
1. **Definitions**.
**“Code”** means the computer software code, whether in human-readable or machine-executable form,
that is delivered by You to Microsoft under this Agreement.
**“Project”** means any of the projects owned or managed by Microsoft and offered under a license
approved by the Open Source Initiative (www.opensource.org).
**“Submit”** is the act of uploading, submitting, transmitting, or distributing code or other content to any
Project, including but not limited to communication on electronic mailing lists, source code control
systems, and issue tracking systems that are managed by, or on behalf of, the Project for the purpose of
discussing and improving that Project, but excluding communication that is conspicuously marked or
otherwise designated in writing by You as “Not a Submission.”
**“Submission”** means the Code and any other copyrightable material Submitted by You, including any
associated comments and documentation.
2. **Your Submission**. You must agree to the terms of this Agreement before making a Submission to any
Project. This Agreement covers any and all Submissions that You, now or in the future (except as
described in Section 4 below), Submit to any Project.
3. **Originality of Work**. You represent that each of Your Submissions is entirely Your original work.
Should You wish to Submit materials that are not Your original work, You may Submit them separately
to the Project if You (a) retain all copyright and license information that was in the materials as You
received them, (b) in the description accompanying Your Submission, include the phrase “Submission
containing materials of a third party:” followed by the names of the third party and any licenses or other
restrictions of which You are aware, and (c) follow any other instructions in the Project’s written
guidelines concerning Submissions.
4. **Your Employer**. References to “employer” in this Agreement include Your employer or anyone else
for whom You are acting in making Your Submission, e.g. as a contractor, vendor, or agent. If Your
Submission is made in the course of Your work for an employer or Your employer has intellectual
property rights in Your Submission by contract or applicable law, You must secure permission from Your
employer to make the Submission before signing this Agreement. In that case, the term “You” in this
Agreement will refer to You and the employer collectively. If You change employers in the future and
desire to Submit additional Submissions for the new employer, then You agree to sign a new Agreement
and secure permission from the new employer before Submitting those Submissions.
5. **Licenses**.
  * **Copyright License**. You grant Microsoft, and those who receive the Submission directly or
indirectly from Microsoft, a perpetual, worldwide, non-exclusive, royalty-free, irrevocable license in the
Submission to reproduce, prepare derivative works of, publicly display, publicly perform, and distribute
the Submission and such derivative works, and to sublicense any or all of the foregoing rights to third
parties.
  * **Patent License**. You grant Microsoft, and those who receive the Submission directly or
indirectly from Microsoft, a perpetual, worldwide, non-exclusive, royalty-free, irrevocable license under
Your patent claims that are necessarily infringed by the Submission or the combination of the
Submission with the Project to which it was Submitted to make, have made, use, offer to sell, sell and
import or otherwise dispose of the Submission alone or with the Project.
  * **Other Rights Reserved**. Each party reserves all rights not expressly granted in this Agreement.
No additional licenses or rights whatsoever (including, without limitation, any implied licenses) are
granted by implication, exhaustion, estoppel or otherwise.
6. **Representations and Warranties**. You represent that You are legally entitled to grant the above
licenses. You represent that each of Your Submissions is entirely Your original work (except as You may
have disclosed under Section 3). You represent that You have secured permission from Your employer to
make the Submission in cases where Your Submission is made in the course of Your work for Your
employer or Your employer has intellectual property rights in Your Submission by contract or applicable
law. If You are signing this Agreement on behalf of Your employer, You represent and warrant that You
have the necessary authority to bind the listed employer to the obligations contained in this Agreement.
You are not expected to provide support for Your Submission, unless You choose to do so. UNLESS
REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING, AND EXCEPT FOR THE WARRANTIES
EXPRESSLY STATED IN SECTIONS 3, 4, AND 6, THE SUBMISSION PROVIDED UNDER THIS AGREEMENT IS
PROVIDED WITHOUT WARRANTY OF ANY KIND, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY OF
NONINFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
7. **Notice to Microsoft**. You agree to notify Microsoft in writing of any facts or circumstances of which
You later become aware that would make Your representations in this Agreement inaccurate in any
respect.
8. **Information about Submissions**. You agree that contributions to Projects and information about
contributions may be maintained indefinitely and disclosed publicly, including Your name and other
information that You submit with Your Submission.
9. **Governing Law/Jurisdiction**. This Agreement is governed by the laws of the State of Washington, and
the parties consent to exclusive jurisdiction and venue in the federal courts sitting in King County,
Washington, unless no federal subject matter jurisdiction exists, in which case the parties consent to
exclusive jurisdiction and venue in the Superior Court of King County, Washington. The parties waive all
defenses of lack of personal jurisdiction and forum non-conveniens.
10. **Entire Agreement/Assignment**. This Agreement is the entire agreement between the parties, and
supersedes any and all prior agreements, understandings or communications, written or oral, between
the parties relating to the subject matter hereof. This Agreement may be assigned by Microsoft.

</details>
<!-- Policy app identification https://img.shields.io/static/v1?label=ContributorLicenseAgreement. -->
vxel: @microsoft-github-policy-service agree company="Oniryx"


lilgreenbird: @vxel thank you for submitting this, the team will review this when time permits
lilgreenbird: took a quick glance at this, please add tests covering the added feature, thanks!
vxel: > took a quick glance at this, please add tests covering the added feature, thanks!

The code is already covered by the `BatchExecutionWithBulkCopyTest.testNullGuid()` and `StatementTest.testJdbc41CallableStatementMethods()` methods, so I wonder which kind of specific test you request.
I guess I can make tests similar to those in `BigIntegerTest` ?
vxel: I just pushed some specific tests
tkyc: @vxel Thanks, I'll need to some testing on our private pipelines. 
tkyc: Testing looks good from internal runs. A question more for the team, is there anywhere else that test can be placed rather than in a new test file?
vxel: Considering the test classes from the `com.microsoft.sqlserver.jdbc.datatypes package`, it seems the practice is to use individual test files for each "special" datatype, e.g. `BigIntegerTest` (for `java.math.BigInteger`), `DateAndTimeTypeTest` (for temporal datatypes), `SQLServerSpatialDatatypeTest` (for Geometry / Geography classes). Hence I guess adding a new test file for the `java.util.UUID datatype` is also suitable. 
Another test file I considered was `com.microsoft.sqlserver.jdbc.preparedStatement.SetObjectTest` but the class is annotated with `@Tag(Constants.xAzureSQLDW)` which I guess would unnecessarily retrict the scope for my GUID tests.
I do not see alternatives but you certainly know the driver better than me.
Please tell me if you find a better place and I will move the test.


Patch:
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index dc42ea42..c4908ce5 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -1,14 +1,3 @@
-# For most projects, this workflow file will not need changing; you simply need
-# to commit it to your repository.
-#
-# You may wish to alter this file to override the set of languages analyzed,
-# or to provide custom queries or build logic.
-#
-# ******** NOTE ********
-# We have attempted to detect the languages in your repository. Please check
-# the `language` matrix defined below to confirm you have the correct set of
-# supported CodeQL languages.
-#
 name: "CodeQL"
 
 on:
@@ -18,8 +7,6 @@ on:
   pull_request:
     # The branches below must be a subset of the branches above
     branches: [ "main" ]
-  schedule:
-    - cron: '27 5 * * 2'
 
 jobs:
   analyze:
@@ -34,16 +21,14 @@ jobs:
       fail-fast: false
       matrix:
         language: [ 'java' ]
-        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
-        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@v2
+      uses: github/codeql-action/init@v3
       with:
         languages: ${{ matrix.language }}
         # If you wish to specify custom queries, you can do so here or in a config file.
@@ -56,7 +41,7 @@ jobs:
     # Autobuild attempts to build any compiled languages  (C/C++, C#, Go, or Java).
     # If this step fails, then you should remove it and run the build manually (see below)
     #- name: Autobuild
-    #  uses: github/codeql-action/autobuild@v2
+    #  uses: github/codeql-action/autobuild@v3
 
     # ℹ️ Command-line programs to run using the OS shell.
     # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -67,9 +52,10 @@ jobs:
     # - run: |
     #   echo "Run, Build Application using script"
     #   ./location_of_script_within_repo/buildscript.sh
+
     - run: mvn install -Pjre11 -Denforcer.skip -Dmaven.javadoc.skip -DskipTests -Dmaven.test.skip.exec
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v2
+      uses: github/codeql-action/analyze@v3
       with:
         category: "/language:${{matrix.language}}"
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
index e5e175b9..597bc962 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
@@ -57,6 +57,7 @@ import java.util.Properties;
 import java.util.Set;
 import java.util.SimpleTimeZone;
 import java.util.TimeZone;
+import java.util.UUID;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.SynchronousQueue;
 import java.util.concurrent.ThreadPoolExecutor;
@@ -1440,7 +1441,7 @@ final class TDSChannel implements Serializable {
         }
 
         @Override
-        public synchronized int getReceiveBufferSize() throws SocketException {
+        public int getReceiveBufferSize() throws SocketException {
             return tdsChannel.tcpSocket.getReceiveBufferSize();
         }
 
@@ -1455,7 +1456,7 @@ final class TDSChannel implements Serializable {
         }
 
         @Override
-        public synchronized int getSendBufferSize() throws SocketException {
+        public int getSendBufferSize() throws SocketException {
             return tdsChannel.tcpSocket.getSendBufferSize();
         }
 
@@ -1465,7 +1466,7 @@ final class TDSChannel implements Serializable {
         }
 
         @Override
-        public synchronized int getSoTimeout() throws SocketException {
+        public int getSoTimeout() throws SocketException {
             return tdsChannel.tcpSocket.getSoTimeout();
         }
 
@@ -1536,19 +1537,19 @@ final class TDSChannel implements Serializable {
         // Ignore calls to methods that would otherwise allow the SSL socket
         // to directly manipulate the underlying TCP socket
         @Override
-        public synchronized void close() throws IOException {
+        public void close() throws IOException {
             if (logger.isLoggable(Level.FINER))
                 logger.finer(logContext + " Ignoring close");
         }
 
         @Override
-        public synchronized void setReceiveBufferSize(int size) throws SocketException {
+        public void setReceiveBufferSize(int size) throws SocketException {
             if (logger.isLoggable(Level.FINER))
                 logger.finer(toString() + " Ignoring setReceiveBufferSize size:" + size);
         }
 
         @Override
-        public synchronized void setSendBufferSize(int size) throws SocketException {
+        public void setSendBufferSize(int size) throws SocketException {
             if (logger.isLoggable(Level.FINER))
                 logger.finer(toString() + " Ignoring setSendBufferSize size:" + size);
         }
@@ -1566,7 +1567,7 @@ final class TDSChannel implements Serializable {
         }
 
         @Override
-        public synchronized void setSoTimeout(int timeout) throws SocketException {
+        public void setSoTimeout(int timeout) throws SocketException {
             tdsChannel.tcpSocket.setSoTimeout(timeout);
         }
 
@@ -4784,6 +4785,32 @@ final class TDSWriter {
         writeBytes(val, 0, val.length);
     }
 
+    /**
+     * Append a UUID in RPC transmission format.
+     *
+     * @param sName
+     *        the optional parameter name
+     * @param uuidValue
+     *        the data value
+     * @param bOut
+     *        boolean true if the data value is being registered as an output parameter
+     */
+    void writeRPCUUID(String sName, UUID uuidValue, boolean bOut) throws SQLServerException {
+        writeRPCNameValType(sName, bOut, TDSType.GUID);
+
+        if (uuidValue == null) {
+            writeByte((byte) 0);
+            writeByte((byte) 0);
+
+        } else {
+            writeByte((byte) 0x10);  // maximum length = 16
+            writeByte((byte) 0x10);  // length = 16
+
+            byte[] val = Util.asGuidByteArray(uuidValue);
+            writeBytes(val, 0, val.length);
+        }
+    }
+
     /**
      * Appends a standard v*max header for RPC parameter transmission.
      * 
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java b/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java
index 4894c45f..6b61cf53 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java
@@ -22,6 +22,7 @@ import java.time.OffsetDateTime;
 import java.time.OffsetTime;
 import java.util.Calendar;
 import java.util.Locale;
+import java.util.UUID;
 
 
 /**
@@ -1125,6 +1126,10 @@ final class Parameter {
             setTypeDefinition(dtv);
         }
 
+        void execute(DTV dtv, UUID uuidValue) throws SQLServerException {
+            setTypeDefinition(dtv);
+        }
+
         void execute(DTV dtv, byte[] byteArrayValue) throws SQLServerException {
             // exclude JDBC typecasting for Geometry/Geography as these datatypes don't have a size limit.
             if (null != byteArrayValue && byteArrayValue.length > DataTypes.SHORT_VARTYPE_MAX_BYTES
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/PersistentTokenCacheAccessAspect.java b/src/main/java/com/microsoft/sqlserver/jdbc/PersistentTokenCacheAccessAspect.java
index 31b87fb2..e388ac90 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/PersistentTokenCacheAccessAspect.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/PersistentTokenCacheAccessAspect.java
@@ -22,13 +22,16 @@ import java.util.concurrent.locks.ReentrantLock;
  * @see <a href="https://aka.ms/msal4j-token-cache">https://aka.ms/msal4j-token-cache</a>
  */
 public class PersistentTokenCacheAccessAspect implements ITokenCacheAccessAspect {
-    private static PersistentTokenCacheAccessAspect instance = new PersistentTokenCacheAccessAspect();
-
+    private static PersistentTokenCacheAccessAspect instance;
     private final Lock lock = new ReentrantLock();
 
-    private PersistentTokenCacheAccessAspect() {}
+    static final long TIME_TO_LIVE = 86400000L; // Token cache time to live (24 hrs).
+    private long expiryTime;
 
     static PersistentTokenCacheAccessAspect getInstance() {
+        if (instance == null) {
+            instance = new PersistentTokenCacheAccessAspect();
+        }
         return instance;
     }
 
@@ -62,6 +65,14 @@ public class PersistentTokenCacheAccessAspect implements ITokenCacheAccessAspect
 
     }
 
+    public long getExpiryTime() {
+        return this.expiryTime;
+    }
+
+    public void setExpiryTime(long expiryTime) {
+        this.expiryTime = expiryTime;
+    }
+
     /**
      * Clears User token cache. This will clear all account info so interactive login will be required on the next
      * request to acquire an access token.
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java
index c2f51aef..e5fa8825 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java
@@ -2064,7 +2064,8 @@ public class SQLServerBulkCopy implements java.lang.AutoCloseable, java.io.Seria
 
     private void writeColumnToTdsWriter(TDSWriter tdsWriter, int bulkPrecision, int bulkScale, int bulkJdbcType,
             boolean bulkNullable, // should it be destNullable instead?
-            int srcColOrdinal, int destColOrdinal, boolean isStreaming, Object colValue, Calendar cal) throws SQLServerException {
+            int srcColOrdinal, int destColOrdinal, boolean isStreaming, Object colValue,
+            Calendar cal) throws SQLServerException {
         SSType destSSType = destColumnMetadata.get(destColOrdinal).ssType;
 
         bulkPrecision = validateSourcePrecision(bulkPrecision, bulkJdbcType,
@@ -2987,8 +2988,8 @@ public class SQLServerBulkCopy implements java.lang.AutoCloseable, java.io.Seria
     /**
      * Reads the given column from the result set current row and writes the data to tdsWriter.
      */
-    private void writeColumn(TDSWriter tdsWriter, int srcColOrdinal, int destColOrdinal,
-            Object colValue, Calendar cal) throws SQLServerException {
+    private void writeColumn(TDSWriter tdsWriter, int srcColOrdinal, int destColOrdinal, Object colValue,
+            Calendar cal) throws SQLServerException {
         String destName = destColumnMetadata.get(destColOrdinal).columnName;
         int srcPrecision, srcScale, destPrecision, srcJdbcType;
         SSType destSSType = null;
@@ -3640,8 +3641,8 @@ public class SQLServerBulkCopy implements java.lang.AutoCloseable, java.io.Seria
                 // Loop for each destination column. The mappings is a many to one mapping
                 // where multiple source columns can be mapped to one destination column.
                 for (ColumnMapping columnMapping : columnMappings) {
-                    writeColumn(tdsWriter, columnMapping.sourceColumnOrdinal, columnMapping.destinationColumnOrdinal, null,
-                            null // cell
+                    writeColumn(tdsWriter, columnMapping.sourceColumnOrdinal, columnMapping.destinationColumnOrdinal,
+                            null, null // cell
                     // value is
                     // retrieved
                     // inside
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
index afe161d3..b20b6e7f 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
@@ -3222,6 +3222,21 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         return this;
     }
 
+    // log open connection failures
+    private void logConnectFailure(int attemptNumber, SQLServerException e, SQLServerError sqlServerError) {
+        loggerResiliency.finer(toString() + " Connection open - connection failed on attempt: " + attemptNumber + ".");
+
+        if (e != null) {
+            loggerResiliency.finer(
+                    toString() + " Connection open - connection failure. Driver error code: " + e.getDriverErrorCode());
+        }
+
+        if (null != sqlServerError && !sqlServerError.getErrorMessage().isEmpty()) {
+            loggerResiliency.finer(toString() + " Connection open - connection failure. SQL Server error : "
+                    + sqlServerError.getErrorMessage());
+        }
+    }
+
     /**
      * This function is used by non failover and failover cases. Even when we make a standard connection the server can
      * provide us with its FO partner. If no FO information is available a standard connection is made. If the server
@@ -3237,6 +3252,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         int fedauthRetryInterval = BACKOFF_INTERVAL; // milliseconds to sleep (back off) between attempts.
 
         long timeoutUnitInterval;
+        long timeForFirstTry = 0; // time it took to do 1st try in ms
 
         boolean useFailoverHost = false;
         FailoverInfo tempFailover = null;
@@ -3434,34 +3450,24 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                                     + connectRetryCount + " reached.");
                 }
 
-                int errorCode = e.getErrorCode();
-                int driverErrorCode = e.getDriverErrorCode();
+                // estimate time it took to do 1 try
+                if (attemptNumber == 0) {
+                    timeForFirstTry = (System.currentTimeMillis() - timerStart);
+                }
+
                 sqlServerError = e.getSQLServerError();
 
-                if (SQLServerException.LOGON_FAILED == errorCode // logon failed, ie bad password
-                        || SQLServerException.PASSWORD_EXPIRED == errorCode // password expired
-                        || SQLServerException.USER_ACCOUNT_LOCKED == errorCode // user account locked
-                        || SQLServerException.DRIVER_ERROR_INVALID_TDS == driverErrorCode // invalid TDS
-                        || SQLServerException.DRIVER_ERROR_SSL_FAILED == driverErrorCode // SSL failure
-                        || SQLServerException.DRIVER_ERROR_INTERMITTENT_TLS_FAILED == driverErrorCode // TLS1.2 failure
-                        || SQLServerException.DRIVER_ERROR_UNSUPPORTED_CONFIG == driverErrorCode // unsupported config
-                                                                                                 // (eg Sphinx, invalid
-                                                                                                 // packetsize, etc)
-                        || (SQLServerException.ERROR_SOCKET_TIMEOUT == driverErrorCode // socket timeout
-                                && (!isDBMirroring || attemptNumber > 0)) // If mirroring, only close after failover has been tried (attempt >= 1)
-                        || timerHasExpired(timerExpire)
-                // for non-dbmirroring cases, do not retry after tcp socket connection succeeds
+                if (isFatalError(e) // do not retry on fatal errors
+                        || timerHasExpired(timerExpire) // no time left
+                        || (timerRemaining(timerExpire) < TimeUnit.SECONDS.toMillis(connectRetryInterval)
+                                + 2 * timeForFirstTry) // not enough time for another retry
+                        || (connectRetryCount == 0 && !isDBMirroring && !useTnir) // retries disabled
+                        // retry at least once for TNIR and failover
+                        || (connectRetryCount == 0 && (isDBMirroring || useTnir) && attemptNumber > 0)
+                        || (connectRetryCount != 0 && attemptNumber >= connectRetryCount) // no retries left
                 ) {
                     if (loggerResiliency.isLoggable(Level.FINER)) {
-                        loggerResiliency.finer(
-                                toString() + " Connection open - connection failed on attempt: " + attemptNumber + ".");
-                        loggerResiliency.finer(toString() + " Connection open - connection failure. Driver error code: "
-                                + driverErrorCode);
-                        if (null != sqlServerError && !sqlServerError.getErrorMessage().isEmpty()) {
-                            loggerResiliency
-                                    .finer(toString() + " Connection open - connection failure. SQL Server error : "
-                                            + sqlServerError.getErrorMessage());
-                        }
+                        logConnectFailure(attemptNumber, e, sqlServerError);
                     }
 
                     // close the connection and throw the error back
@@ -3469,15 +3475,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                     throw e;
                 } else {
                     if (loggerResiliency.isLoggable(Level.FINER)) {
-                        loggerResiliency.finer(
-                                toString() + " Connection open - connection failed on attempt: " + attemptNumber + ".");
-                        loggerResiliency.finer(toString() + " Connection open - connection failure. Driver error code: "
-                                + driverErrorCode);
-                        if (null != sqlServerError && !sqlServerError.getErrorMessage().isEmpty()) {
-                            loggerResiliency
-                                    .finer(toString() + " Connection open - connection failure. SQL Server error : "
-                                            + sqlServerError.getErrorMessage());
-                        }
+                        logConnectFailure(attemptNumber, e, sqlServerError);
                     }
 
                     // Close the TDS channel from the failed connection attempt so that we don't
@@ -3496,15 +3494,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                     if (remainingMilliseconds <= fedauthRetryInterval) {
 
                         if (loggerResiliency.isLoggable(Level.FINER)) {
-                            loggerResiliency.finer(toString() + " Connection open - connection failed on attempt: "
-                                    + attemptNumber + ".");
-                            loggerResiliency.finer(toString()
-                                    + " Connection open - connection failure. Driver error code: " + driverErrorCode);
-                            if (null != sqlServerError && !sqlServerError.getErrorMessage().isEmpty()) {
-                                loggerResiliency
-                                        .finer(toString() + " Connection open - connection failure. SQL Server error : "
-                                                + sqlServerError.getErrorMessage());
-                            }
+                            logConnectFailure(attemptNumber, e, sqlServerError);
                         }
 
                         throw e;
@@ -3517,19 +3507,22 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
             // the network with requests, then update sleep interval for next iteration (max 1 second interval)
             // We have to sleep for every attempt in case of non-dbMirroring scenarios (including multisubnetfailover),
             // Whereas for dbMirroring, we sleep for every two attempts as each attempt is to a different server.
-            if (!isDBMirroring || (1 == attemptNumber % 2)) {
-                if (loggerResiliency.isLoggable(Level.FINER)) {
-                    loggerResiliency.finer(toString() + " Connection open - sleeping milisec: " + connectRetryInterval);
-                }
-                if (loggerResiliency.isLoggable(Level.FINER)) {
-                    loggerResiliency.finer(toString() + " Connection open - connection failed on transient error "
-                            + (sqlServerError != null ? sqlServerError.getErrorNumber() : "")
-                            + ". Wait for connectRetryInterval(" + connectRetryInterval + ")s before retry #"
-                            + attemptNumber);
-                }
+            // Make sure there's enough time to do another retry
+            if (!isDBMirroring || (isDBMirroring && (0 == attemptNumber % 2))
+                    && (attemptNumber < connectRetryCount && connectRetryCount != 0) && timerRemaining(
+                            timerExpire) > (TimeUnit.SECONDS.toMillis(connectRetryInterval) + 2 * timeForFirstTry)) {
+
+                // don't wait for TNIR
+                if (!(useTnir && attemptNumber == 0)) {
+                    if (loggerResiliency.isLoggable(Level.FINER)) {
+                        loggerResiliency.finer(toString() + " Connection open - connection failed on transient error "
+                                + (sqlServerError != null ? sqlServerError.getErrorNumber() : "")
+                                + ". Wait for connectRetryInterval(" + connectRetryInterval + ")s before retry #"
+                                + attemptNumber);
+                    }
 
-                sleepForInterval(fedauthRetryInterval);
-                fedauthRetryInterval = (fedauthRetryInterval < 500) ? fedauthRetryInterval * 2 : 1000;
+                    sleepForInterval(TimeUnit.SECONDS.toMillis(connectRetryInterval));
+                }
             }
 
             // Update timeout interval (but no more than the point where we're supposed to fail: timerExpire)
@@ -3622,31 +3615,22 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         }
     }
 
+    // non recoverable or retryable fatal errors
     boolean isFatalError(SQLServerException e) {
         /*
          * NOTE: If these conditions are modified, consider modification to conditions in SQLServerConnection::login()
          * and Reconnect::run()
          */
+        int errorCode = e.getErrorCode();
+        int driverErrorCode = e.getDriverErrorCode();
 
-        // 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;
+        return ((SQLServerException.LOGON_FAILED == errorCode) // logon failed (eg bad password)
+                || (SQLServerException.PASSWORD_EXPIRED == errorCode) // password expired
+                || (SQLServerException.USER_ACCOUNT_LOCKED == errorCode) // user account locked
+                || (SQLServerException.DRIVER_ERROR_INVALID_TDS == driverErrorCode) // invalid TDS from server
+                || (SQLServerException.DRIVER_ERROR_SSL_FAILED == driverErrorCode) // SSL failure
+                || (SQLServerException.DRIVER_ERROR_INTERMITTENT_TLS_FAILED == driverErrorCode) // TLS1.2 failure
+                || (SQLServerException.DRIVER_ERROR_UNSUPPORTED_CONFIG == driverErrorCode)); // unsupported config (eg Sphinx, invalid packet size ,etc)
     }
 
     // reset all params that could have been changed due to ENVCHANGE tokens to defaults,
@@ -3707,7 +3691,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
     }
 
     /**
-     * Get time remaining to timer expiry
+     * Get time remaining to timer expiry (in ms)
      * 
      * @param timerExpire
      * @return remaining time to expiry
@@ -5979,7 +5963,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         String user = activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString());
 
         // No of milliseconds to sleep for the initial back off.
-        int sleepInterval = BACKOFF_INTERVAL;
+        int fedauthSleepInterval = BACKOFF_INTERVAL;
 
         if (!msalContextExists()
                 && !authenticationString.equalsIgnoreCase(SqlAuthentication.ACTIVE_DIRECTORY_INTEGRATED.toString())) {
@@ -6078,7 +6062,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
 
                         int millisecondsRemaining = timerRemaining(timerExpire);
                         if (ActiveDirectoryAuthentication.GET_ACCESS_TOKEN_TRANSIENT_ERROR != errorCategory
-                                || timerHasExpired(timerExpire) || (sleepInterval >= millisecondsRemaining)) {
+                                || timerHasExpired(timerExpire) || (fedauthSleepInterval >= millisecondsRemaining)) {
 
                             String errorStatus = Integer.toHexString(adalException.getStatus());
 
@@ -6102,13 +6086,14 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
 
                         if (connectionlogger.isLoggable(Level.FINER)) {
                             connectionlogger.fine(toString() + " SQLServerConnection.getFedAuthToken sleeping: "
-                                    + sleepInterval + " milliseconds.");
+                                    + fedauthSleepInterval + " milliseconds.");
                             connectionlogger.fine(toString() + " SQLServerConnection.getFedAuthToken remaining: "
                                     + millisecondsRemaining + " milliseconds.");
                         }
 
-                        sleepForInterval(sleepInterval);
-                        sleepInterval = sleepInterval * 2;
+                        sleepForInterval(fedauthSleepInterval);
+                        fedauthSleepInterval = (fedauthSleepInterval < 500) ? fedauthSleepInterval * 2 : 1000;
+
                     }
                 }
                 // else choose MSAL4J for integrated authentication. This option is supported for both windows and unix,
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java
index 1d286887..2dfb40d8 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java
@@ -258,11 +258,17 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
     private static final String IS_AUTOINCREMENT = "IS_AUTOINCREMENT";
     private static final String ACTIVITY_ID = " ActivityId: ";
 
+    private static final String NVARCHAR = JDBCType.NVARCHAR.name();
+    private static final String VARCHAR = JDBCType.VARCHAR.name();
+    private static final String INTEGER = JDBCType.INTEGER.name();
+    private static final String SMALLINT = JDBCType.SMALLINT.name();
+
     private static final String SQL_KEYWORDS = createSqlKeyWords();
 
     // Use LinkedHashMap to force retrieve elements in order they were inserted
     /** getColumns columns */
     private LinkedHashMap<Integer, String> getColumnsDWColumns = null;
+    private LinkedHashMap<Integer, String> getTypesDWColumns = null;
     /** getImportedKeys columns */
     private volatile LinkedHashMap<Integer, String> getImportedKeysDWColumns;
     private static final Lock LOCK = new ReentrantLock();
@@ -630,10 +636,13 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
 
                     + "INSERT INTO @mssqljdbc_temp_sp_columns_result EXEC sp_columns_100 ?,?,?,?,?,?;"
 
-                    + "SELECT TABLE_QUALIFIER AS TABLE_CAT, TABLE_OWNER AS TABLE_SCHEM, TABLE_NAME, COLUMN_NAME, DATA_TYPE,"
-                    + "TYPE_NAME, PRECISION AS COLUMN_SIZE, LENGTH AS BUFFER_LENGTH, SCALE AS DECIMAL_DIGITS, RADIX AS NUM_PREC_RADIX,"
-                    + "NULLABLE, REMARKS, COLUMN_DEF, SQL_DATA_TYPE, SQL_DATETIME_SUB, CHAR_OCTET_LENGTH, ORDINAL_POSITION, IS_NULLABLE,"
-                    + "NULL AS SCOPE_CATALOG, NULL AS SCOPE_SCHEMA, NULL AS SCOPE_TABLE, SS_DATA_TYPE AS SOURCE_DATA_TYPE,"
+                    + "SELECT TABLE_QUALIFIER AS TABLE_CAT, TABLE_OWNER AS TABLE_SCHEM, TABLE_NAME, COLUMN_NAME, "
+                    + "CAST(DATA_TYPE AS INT) AS DATA_TYPE,TYPE_NAME, PRECISION AS COLUMN_SIZE, LENGTH AS BUFFER_LENGTH, "
+                    + "CAST(SCALE AS INT) AS DECIMAL_DIGITS, CAST(RADIX AS INT) AS NUM_PREC_RADIX,CAST(NULLABLE AS INT) AS NULLABLE, "
+                    + "CAST(REMARKS AS VARCHAR) AS REMARKS, COLUMN_DEF, CAST(SQL_DATA_TYPE AS INT) AS SQL_DATA_TYPE, "
+                    + "CAST(SQL_DATETIME_SUB AS INT) AS SQL_DATETIME_SUB, CHAR_OCTET_LENGTH, ORDINAL_POSITION, IS_NULLABLE,"
+                    + "CAST(NULL AS VARCHAR) AS SCOPE_CATALOG, CAST(NULL AS VARCHAR) AS SCOPE_SCHEMA, CAST(NULL AS VARCHAR) AS SCOPE_TABLE, "
+                    + "CAST(SS_DATA_TYPE AS SMALLINT) AS SOURCE_DATA_TYPE, "
                     + "CASE SS_IS_IDENTITY WHEN 0 THEN 'NO' WHEN 1 THEN 'YES' WHEN '' THEN '' END AS IS_AUTOINCREMENT,"
                     + "CASE SS_IS_COMPUTED WHEN 0 THEN 'NO' WHEN 1 THEN 'YES' WHEN '' THEN '' END AS IS_GENERATEDCOLUMN, "
                     + "SS_IS_SPARSE, SS_IS_COLUMN_SET, SS_UDT_CATALOG_NAME, SS_UDT_SCHEMA_NAME, SS_UDT_ASSEMBLY_TYPE_NAME,"
@@ -721,6 +730,53 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
                     getColumnsDWColumns.put(27, SS_XML_SCHEMACOLLECTION_SCHEMA_NAME);
                     getColumnsDWColumns.put(28, SS_XML_SCHEMACOLLECTION_NAME);
                 }
+                if (null == getTypesDWColumns) {
+                    getTypesDWColumns = new LinkedHashMap<>();
+                    getTypesDWColumns.put(1, NVARCHAR); // TABLE_CAT
+                    getTypesDWColumns.put(2, NVARCHAR); // TABLE_SCHEM
+                    getTypesDWColumns.put(3, NVARCHAR); // TABLE_NAME
+                    getTypesDWColumns.put(4, NVARCHAR); // COLUMN_NAME
+                    getTypesDWColumns.put(5, INTEGER); // DATA_TYPE
+                    getTypesDWColumns.put(6, NVARCHAR); // TYPE_NAME
+                    getTypesDWColumns.put(7, INTEGER); // COLUMN_SIZE
+                    getTypesDWColumns.put(8, INTEGER); // BUFFER_LENGTH
+                    getTypesDWColumns.put(9, INTEGER); // DECIMAL_DIGITS
+                    getTypesDWColumns.put(10, INTEGER); // NUM_PREC_RADIX
+                    getTypesDWColumns.put(11, INTEGER); // NULLABLE
+                    getTypesDWColumns.put(12, VARCHAR); // REMARKS
+                    getTypesDWColumns.put(13, NVARCHAR); // COLUMN_DEF
+                    getTypesDWColumns.put(14, INTEGER); // SQL_DATA_TYPE
+                    getTypesDWColumns.put(15, INTEGER); // SQL_DATETIME_SUB
+                    getTypesDWColumns.put(16, INTEGER); // CHAR_OCTET_LENGTH
+                    getTypesDWColumns.put(17, INTEGER); // ORDINAL_POSITION
+                    getTypesDWColumns.put(18, VARCHAR); // IS_NULLABLE
+                    /*
+                     * Use negative value keys to indicate that this column doesn't exist in SQL Server and should just
+                     * be queried as 'NULL'
+                     */
+                    getTypesDWColumns.put(-1, VARCHAR); // SCOPE_CATALOG
+                    getTypesDWColumns.put(-2, VARCHAR); // SCOPE_SCHEMA
+                    getTypesDWColumns.put(-3, VARCHAR); // SCOPE_TABLE
+                    getTypesDWColumns.put(29, SMALLINT); // SOURCE_DATA_TYPE
+                    getTypesDWColumns.put(22, VARCHAR); // IS_AUTOINCREMENT
+                    getTypesDWColumns.put(21, VARCHAR); // IS_GENERATEDCOLUMN
+                    getTypesDWColumns.put(19, SMALLINT); // SS_IS_SPARSE
+                    getTypesDWColumns.put(20, SMALLINT); // SS_IS_COLUMN_SET
+                    getTypesDWColumns.put(23, NVARCHAR); // SS_UDT_CATALOG_NAME
+                    getTypesDWColumns.put(24, NVARCHAR); // SS_UDT_SCHEMA_NAME
+                    getTypesDWColumns.put(25, NVARCHAR); // SS_UDT_ASSEMBLY_TYPE_NAME
+                    getTypesDWColumns.put(26, NVARCHAR); // SS_XML_SCHEMACOLLECTION_CATALOG_NAME
+                    getTypesDWColumns.put(27, NVARCHAR); // SS_XML_SCHEMACOLLECTION_SCHEMA_NAME
+                    getTypesDWColumns.put(28, NVARCHAR); // SS_XML_SCHEMACOLLECTION_NAME
+                }
+
+                // Ensure there is a data type for every metadata column
+                if (getColumnsDWColumns.size() != getTypesDWColumns.size()) {
+                    MessageFormat form = new MessageFormat(
+                            SQLServerException.getErrString("R_colCountNotMatchColTypeCount"));
+                    Object[] msgArgs = {getColumnsDWColumns.size(), getTypesDWColumns.size()};
+                    throw new IllegalArgumentException(form.format(msgArgs));
+                }
             } finally {
                 LOCK.unlock();
             }
@@ -744,7 +800,7 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
                         if (!isFirstRow) {
                             azureDwSelectBuilder.append(" UNION ALL ");
                         }
-                        azureDwSelectBuilder.append(generateAzureDWSelect(rs, getColumnsDWColumns));
+                        azureDwSelectBuilder.append(generateAzureDWSelect(rs, getColumnsDWColumns, getTypesDWColumns));
                         isFirstRow = false;
                     }
 
@@ -780,28 +836,41 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
         }
     }
 
-    private String generateAzureDWSelect(ResultSet rs, Map<Integer, String> columns) throws SQLException {
+    private String generateAzureDWSelect(ResultSet rs, Map<Integer, String> columns,
+            Map<Integer, String> types) throws SQLException {
         StringBuilder sb = new StringBuilder("SELECT ");
+
         for (Entry<Integer, String> p : columns.entrySet()) {
+            String dataType = types.get(p.getKey());
+
+            // Verify there is a valid column entry in the Data Type lookup map
+            if (dataType == null) {
+                MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidArgument"));
+                Object[] msgArgs = {p.getKey()};
+                throw new SQLServerException(null, form.format(msgArgs), null, 0, true);
+            }
+
+            sb.append("CAST(");
             if (p.getKey() < 0) {
-                sb.append("NULL");
+                sb.append("NULL AS " + dataType);
             } else {
                 Object o = rs.getObject(p.getKey());
                 if (null == o) {
-                    sb.append("NULL");
+                    sb.append("NULL AS " + dataType);
                 } else if (o instanceof Number) {
                     if (IS_AUTOINCREMENT.equalsIgnoreCase(p.getValue())
                             || IS_GENERATEDCOLUMN.equalsIgnoreCase(p.getValue())) {
                         sb.append("'").append(Util.escapeSingleQuotes(Util.zeroOneToYesNo(((Number) o).intValue())))
-                                .append("'");
+                                .append("' AS ").append(dataType);
                     } else {
-                        sb.append(o.toString());
+                        sb.append(o.toString()).append(" AS ").append(dataType);
                     }
                 } else {
-                    sb.append("'").append(Util.escapeSingleQuotes(o.toString())).append("'");
+                    sb.append("'").append(Util.escapeSingleQuotes(o.toString())).append("' AS ").append(dataType)
+                            .append("(").append(Integer.toString(o.toString().length())).append(")");
                 }
             }
-            sb.append(" AS ").append(p.getValue()).append(",");
+            sb.append(") AS ").append(p.getValue()).append(",");
         }
         sb.setLength(sb.length() - 1);
         return sb.toString();
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java
index 7d4fa23f..943804dc 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java
@@ -395,16 +395,17 @@ public final class SQLServerException extends java.sql.SQLException {
     static String checkAndAppendClientConnId(String errMsg, SQLServerConnection conn) {
         if (null != conn && conn.isConnected()) {
             UUID clientConnId = conn.getClientConIdInternal();
-            assert null != clientConnId;
-            StringBuilder sb = new StringBuilder(errMsg);
-            // This syntax of adding connection id is matched in a retry logic. If anything changes here, make
-            // necessary changes to enableSSL() function's exception handling mechanism.
-            sb.append(LOG_CLIENT_CONNECTION_ID_PREFIX);
-            sb.append(clientConnId.toString());
-            return sb.toString();
-        } else {
-            return errMsg;
+            if (null != clientConnId) {
+                StringBuilder sb = (errMsg != null) ? new StringBuilder(errMsg) : new StringBuilder();
+                // This syntax of adding connection id is matched in a retry logic. If anything changes here, make
+                // necessary changes to enableSSL() function's exception handling mechanism.
+                sb.append(LOG_CLIENT_CONNECTION_ID_PREFIX);
+                sb.append(clientConnId.toString());
+                return sb.toString();
+            }
         }
+        return (errMsg != null) ? errMsg : "";
+
     }
 
     static void throwNotSupportedException(SQLServerConnection con, Object obj) throws SQLServerException {
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java
index c8f2fb34..1ae69891 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java
@@ -14,6 +14,7 @@ import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URISyntaxException;
 
+import java.security.MessageDigest;
 import java.text.MessageFormat;
 
 import java.util.Collections;
@@ -21,6 +22,7 @@ import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -61,6 +63,8 @@ class SQLServerMSAL4JUtils {
     static final String SLASH_DEFAULT = "/.default";
     static final String ACCESS_TOKEN_EXPIRE = "access token expires: ";
 
+    private static final TokenCacheMap TOKEN_CACHE_MAP = new TokenCacheMap();
+
     private final static String LOGCONTEXT = "MSAL version "
             + com.microsoft.aad.msal4j.PublicClientApplication.class.getPackage().getImplementationVersion() + ": ";
 
@@ -84,10 +88,17 @@ class SQLServerMSAL4JUtils {
         lock.lock();
 
         try {
+            String hashedSecret = getHashedSecret(new String[] {fedAuthInfo.stsurl, user, password});
+            PersistentTokenCacheAccessAspect persistentTokenCacheAccessAspect = TOKEN_CACHE_MAP.getEntry(hashedSecret);
+
+            if (null == persistentTokenCacheAccessAspect) {
+                persistentTokenCacheAccessAspect = new PersistentTokenCacheAccessAspect();
+                TOKEN_CACHE_MAP.addEntry(hashedSecret, persistentTokenCacheAccessAspect);
+            }
+
             final PublicClientApplication pca = PublicClientApplication
                     .builder(ActiveDirectoryAuthentication.JDBC_FEDAUTH_CLIENT_ID).executorService(executorService)
-                    .setTokenCacheAccessAspect(PersistentTokenCacheAccessAspect.getInstance())
-                    .authority(fedAuthInfo.stsurl).build();
+                    .setTokenCacheAccessAspect(persistentTokenCacheAccessAspect).authority(fedAuthInfo.stsurl).build();
 
             final CompletableFuture<IAuthenticationResult> future = pca.acquireToken(UserNamePasswordParameters
                     .builder(Collections.singleton(fedAuthInfo.spn + SLASH_DEFAULT), user, password.toCharArray())
@@ -132,11 +143,19 @@ class SQLServerMSAL4JUtils {
         lock.lock();
 
         try {
+            String hashedSecret = getHashedSecret(
+                    new String[] {fedAuthInfo.stsurl, aadPrincipalID, aadPrincipalSecret});
+            PersistentTokenCacheAccessAspect persistentTokenCacheAccessAspect = TOKEN_CACHE_MAP.getEntry(hashedSecret);
+
+            if (null == persistentTokenCacheAccessAspect) {
+                persistentTokenCacheAccessAspect = new PersistentTokenCacheAccessAspect();
+                TOKEN_CACHE_MAP.addEntry(hashedSecret, persistentTokenCacheAccessAspect);
+            }
+
             IClientCredential credential = ClientCredentialFactory.createFromSecret(aadPrincipalSecret);
             ConfidentialClientApplication clientApplication = ConfidentialClientApplication
                     .builder(aadPrincipalID, credential).executorService(executorService)
-                    .setTokenCacheAccessAspect(PersistentTokenCacheAccessAspect.getInstance())
-                    .authority(fedAuthInfo.stsurl).build();
+                    .setTokenCacheAccessAspect(persistentTokenCacheAccessAspect).authority(fedAuthInfo.stsurl).build();
 
             final CompletableFuture<IAuthenticationResult> future = clientApplication
                     .acquireToken(ClientCredentialParameters.builder(scopes).build());
@@ -181,6 +200,15 @@ class SQLServerMSAL4JUtils {
         lock.lock();
 
         try {
+            String hashedSecret = getHashedSecret(new String[] {fedAuthInfo.stsurl, aadPrincipalID, certFile,
+                    certPassword, certKey, certKeyPassword});
+            PersistentTokenCacheAccessAspect persistentTokenCacheAccessAspect = TOKEN_CACHE_MAP.getEntry(hashedSecret);
+
+            if (null == persistentTokenCacheAccessAspect) {
+                persistentTokenCacheAccessAspect = new PersistentTokenCacheAccessAspect();
+                TOKEN_CACHE_MAP.addEntry(hashedSecret, persistentTokenCacheAccessAspect);
+            }
+
             ConfidentialClientApplication clientApplication = null;
 
             // check if cert is PKCS12 first
@@ -202,8 +230,7 @@ class SQLServerMSAL4JUtils {
 
                 IClientCredential credential = ClientCredentialFactory.createFromCertificate(is, certPassword);
                 clientApplication = ConfidentialClientApplication.builder(aadPrincipalID, credential)
-                        .executorService(executorService)
-                        .setTokenCacheAccessAspect(PersistentTokenCacheAccessAspect.getInstance())
+                        .executorService(executorService).setTokenCacheAccessAspect(persistentTokenCacheAccessAspect)
                         .authority(fedAuthInfo.stsurl).build();
             } catch (FileNotFoundException e) {
                 // re-throw if file not there no point to try another format
@@ -232,8 +259,7 @@ class SQLServerMSAL4JUtils {
 
                 IClientCredential credential = ClientCredentialFactory.createFromCertificate(privateKey, cert);
                 clientApplication = ConfidentialClientApplication.builder(aadPrincipalID, credential)
-                        .executorService(executorService)
-                        .setTokenCacheAccessAspect(PersistentTokenCacheAccessAspect.getInstance())
+                        .executorService(executorService).setTokenCacheAccessAspect(persistentTokenCacheAccessAspect)
                         .authority(fedAuthInfo.stsurl).build();
             }
 
@@ -449,4 +475,45 @@ class SQLServerMSAL4JUtils {
             return new SQLServerException(form.format(msgArgs), null, 0, correctedExecutionException);
         }
     }
+
+    private static String getHashedSecret(String[] secrets) throws SQLServerException {
+        try {
+            MessageDigest md = MessageDigest.getInstance("SHA-256");
+            for (String secret : secrets) {
+                if (null != secret) {
+                    md.update(secret.getBytes(java.nio.charset.StandardCharsets.UTF_16LE));
+                }
+            }
+            return new String(md.digest());
+        } catch (NoSuchAlgorithmException e) {
+            throw new SQLServerException(SQLServerException.getErrString("R_NoSHA256Algorithm"), e);
+        }
+    }
+
+    private static class TokenCacheMap {
+        private ConcurrentHashMap<String, PersistentTokenCacheAccessAspect> tokenCacheMap = new ConcurrentHashMap<>();
+
+        PersistentTokenCacheAccessAspect getEntry(String key) {
+            PersistentTokenCacheAccessAspect persistentTokenCacheAccessAspect = tokenCacheMap.get(key);
+
+            if (null != persistentTokenCacheAccessAspect) {
+                if (System.currentTimeMillis() > persistentTokenCacheAccessAspect.getExpiryTime()) {
+                    tokenCacheMap.remove(key);
+
+                    persistentTokenCacheAccessAspect = new PersistentTokenCacheAccessAspect();
+                    persistentTokenCacheAccessAspect
+                            .setExpiryTime(System.currentTimeMillis() + PersistentTokenCacheAccessAspect.TIME_TO_LIVE);
+
+                    tokenCacheMap.put(key, persistentTokenCacheAccessAspect);
+                }
+            }
+
+            return persistentTokenCacheAccessAspect;
+        }
+
+        void addEntry(String key, PersistentTokenCacheAccessAspect value) {
+            value.setExpiryTime(System.currentTimeMillis() + PersistentTokenCacheAccessAspect.TIME_TO_LIVE);
+            tokenCacheMap.put(key, value);
+        }
+    }
 }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java
index d5b9b549..294a1aa8 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java
@@ -29,6 +29,7 @@ import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Vector;
 import java.util.logging.Level;
+import java.util.regex.Pattern;
 
 import com.microsoft.sqlserver.jdbc.SQLServerConnection.CityHash128Key;
 import com.microsoft.sqlserver.jdbc.SQLServerConnection.PreparedStatementHandle;
@@ -70,6 +71,12 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements IS
     /** Processed SQL statement text, may not be same as what user initially passed. */
     final String userSQL;
 
+    private boolean isExecEscapeSyntax;
+
+    private boolean isCallEscapeSyntax;
+
+    private boolean isFourPartSyntax;
+
     /** Parameter positions in processed SQL statement text. */
     final int[] userSQLParamPositions;
 
@@ -128,6 +135,22 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements IS
      */
     private boolean useBulkCopyForBatchInsert;
 
+    /**
+     * Regex for JDBC 'call' escape syntax
+     */
+    private static final Pattern callEscapePattern = Pattern
+            .compile("^\\s*(?i)\\{(\\s*\\??\\s*=?\\s*)call (.+)\\s*\\(?\\?*,?\\)?\\s*}\\s*$");
+
+    /**
+     * Regex for 'exec' escape syntax
+     */
+    private static final Pattern execEscapePattern = Pattern.compile("^\\s*(?i)(?:exec|execute)\\b");
+
+    /**
+     * Regex for four part syntax
+     */
+    private static final Pattern fourPartSyntaxPattern = Pattern.compile("(.+)\\.(.+)\\.(.+)\\.(.+)");
+
     /** Returns the prepared statement SQL */
     @Override
     public String toString() {
@@ -253,6 +276,9 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements IS
         procedureName = parsedSQL.procedureName;
         bReturnValueSyntax = parsedSQL.bReturnValueSyntax;
         userSQL = parsedSQL.processedSQL;
+        isExecEscapeSyntax = isExecEscapeSyntax(sql);
+        isCallEscapeSyntax = isCallEscapeSyntax(sql);
+        isFourPartSyntax = isFourPartSyntax(sql);
         userSQLParamPositions = parsedSQL.parameterPositions;
         initParams(userSQLParamPositions.length);
         useBulkCopyForBatchInsert = conn.getUseBulkCopyForBatchInsert();
@@ -1210,7 +1236,18 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements IS
      */
     boolean callRPCDirectly(Parameter[] params) throws SQLServerException {
         int paramCount = SQLServerConnection.countParams(userSQL);
-        return (null != procedureName && paramCount != 0 && !isTVPType(params));
+
+        // In order to execute sprocs directly the following must be true:
+        // 1. There must be a sproc name
+        // 2. There must be parameters
+        // 3. Parameters must not be a TVP type
+        // 4. Compliant CALL escape syntax
+        // If isExecEscapeSyntax is true, EXEC escape syntax is used then use prior behaviour of
+        // wrapping call to execute the procedure
+        // If isFourPartSyntax is true, sproc is being executed against linked server, then
+        // use prior behaviour of wrapping call to execute procedure
+        return (null != procedureName && paramCount != 0 && !isTVPType(params) && isCallEscapeSyntax
+                && !isExecEscapeSyntax && !isFourPartSyntax);
     }
 
     /**
@@ -1230,6 +1267,18 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements IS
         return false;
     }
 
+    private boolean isExecEscapeSyntax(String sql) {
+        return execEscapePattern.matcher(sql).find();
+    }
+
+    private boolean isCallEscapeSyntax(String sql) {
+        return callEscapePattern.matcher(sql).find();
+    }
+
+    private boolean isFourPartSyntax(String sql) {
+        return fourPartSyntaxPattern.matcher(sql).find();
+    }
+
     /**
      * Executes sp_prepare to prepare a parameterized statement and sets the prepared statement handle
      *
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
index 95b5ffa0..6cd502b7 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
@@ -545,7 +545,8 @@ public final class SQLServerResource extends ListResourceBundle {
         {"R_ManagedIdentityTokenAcquisitionFail", "Failed to acquire managed identity token. Request for the token succeeded, but no token was returned. The token is null."},
         {"R_AmbiguousRowUpdate", "Failed to execute updateRow(). The update is attempting an ambiguous update on tables \"{0}\" and \"{1}\". Ensure all columns being updated prior to the updateRow() call belong to the same table."},
         {"R_InvalidSqlQuery", "Invalid SQL Query: {0}"},
-        {"R_InvalidScale", "Scale of input value is larger than the maximum allowed by SQL Server."}
+        {"R_InvalidScale", "Scale of input value is larger than the maximum allowed by SQL Server."},
+        {"R_colCountNotMatchColTypeCount", "Number of provided columns {0} does not match the column data types definition {1}."},
     };
 }
 // @formatter:on
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java
index 6d82bf83..eb96becb 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java
@@ -5,7 +5,6 @@
 
 package com.microsoft.sqlserver.jdbc;
 
-import java.net.SocketException;
 import java.sql.CallableStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
@@ -25,7 +24,6 @@ import javax.transaction.xa.XAException;
 import javax.transaction.xa.XAResource;
 import javax.transaction.xa.Xid;
 
-import com.microsoft.sqlserver.jdbc.SQLServerError.TransientError;
 
 
 /**
@@ -754,27 +752,13 @@ public final class SQLServerXAResource implements javax.transaction.xa.XAResourc
                     }
                 }
             }
-        } catch (SQLTimeoutException ex) {
+        } catch (SQLTimeoutException | SQLServerException ex) {
             if (xaLogger.isLoggable(Level.FINER))
                 xaLogger.finer(toString() + " exception:" + ex);
             XAException e = new XAException(ex.toString());
             e.errorCode = XAException.XAER_RMFAIL;
             throw e;
 
-        } catch (SQLServerException ex) {
-            if (xaLogger.isLoggable(Level.FINER))
-                xaLogger.finer(toString() + " exception:" + ex);
-
-            if (ex.getMessage().equals(SQLServerException.getErrString("R_noServerResponse"))
-                    || TransientError.isTransientError(ex.getSQLServerError()) || isResourceManagerFailure(ex)) {
-                XAException e = new XAException(ex.toString());
-                e.errorCode = XAException.XAER_RMFAIL;
-                throw e;
-            }
-
-            XAException e = new XAException(ex.toString());
-            e.errorCode = XAException.XAER_RMERR;
-            throw e;
         }
 
         if (xaLogger.isLoggable(Level.FINER))
@@ -945,68 +929,4 @@ public final class SQLServerXAResource implements javax.transaction.xa.XAResourc
         return baseResourceID.incrementAndGet();
     }
 
-    private enum ResourceManagerFailure {
-        CONN_RESET("Connection reset"),
-        CONN_RESET_BY_PEER("Connection reset by peer"),
-        CONN_TIMEOUT("Connection timed out"),
-        CONN_RESILIENCY_CLIENT_UNRECOVERABLE(SQLServerException.getErrString("R_crClientUnrecoverable"));
-
-        private final String errString;
-
-        ResourceManagerFailure(String errString) {
-            this.errString = errString;
-        }
-
-        @Override
-        public String toString() {
-            return errString;
-        }
-
-        static ResourceManagerFailure fromString(String errString) {
-            for (ResourceManagerFailure resourceManagerFailure : ResourceManagerFailure.values()) {
-                if (errString.equalsIgnoreCase(resourceManagerFailure.toString())) {
-                    return resourceManagerFailure;
-                }
-            }
-            return null;
-        }
-    }
-
-    /**
-     * Check if the root exception of the throwable should be a XAER_RMFAIL exception
-     *
-     * @param throwable
-     *        The exception to check if the root cause should be a XAER_RMFAIL
-     *
-     * @return True if XAER_RMFAIL, otherwise false
-     */
-    private boolean isResourceManagerFailure(Throwable throwable) {
-        Throwable root = Util.getRootCause(throwable);
-
-        if (null == root) {
-            return false;
-        }
-
-        if (xaLogger.isLoggable(Level.FINE)) {
-            xaLogger.fine(toString() + " Resource manager failure root exception: " + root);
-        }
-
-        ResourceManagerFailure err = ResourceManagerFailure.fromString(root.getMessage());
-
-        if (null == err) {
-            return false;
-        }
-
-        // Add as needed here for future XAER_RMFAIL exceptions
-        switch (err) {
-            case CONN_RESET:
-            case CONN_RESET_BY_PEER:
-            case CONN_TIMEOUT:
-            case CONN_RESILIENCY_CLIENT_UNRECOVERABLE:
-                return true;
-            default:
-                return false;
-        }
-    }
-
 }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java
index 70b9c9aa..61bda22b 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java
@@ -13,8 +13,6 @@ import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.text.DecimalFormat;
 import java.text.MessageFormat;
-import java.util.List;
-import java.util.ArrayList;
 import java.util.Date;
 import java.util.Locale;
 import java.util.Properties;
@@ -1046,23 +1044,6 @@ final class Util {
         }
         return chars;
     }
-
-    /**
-     * @param throwable
-     *        The exception to find root cause for.
-     *
-     * @return The root cause of the exception, otherwise null if null throwable input
-     */
-    static Throwable getRootCause(Throwable throwable) {
-        final List<Throwable> list = new ArrayList<>();
-
-        while (throwable != null && !list.contains(throwable)) {
-            list.add(throwable);
-            throwable = throwable.getCause();
-        }
-
-        return list.isEmpty() ? null : list.get(list.size() - 1);
-    }
 }
 
 
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java
index c03d67eb..32a1c762 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java
@@ -99,6 +99,8 @@ abstract class DTVExecuteOp {
 
     abstract void execute(DTV dtv, Boolean booleanValue) throws SQLServerException;
 
+    abstract void execute(DTV dtv, UUID uuidValue) throws SQLServerException;
+
     abstract void execute(DTV dtv, byte[] byteArrayValue) throws SQLServerException;
 
     abstract void execute(DTV dtv, Blob blobValue) throws SQLServerException;
@@ -291,7 +293,11 @@ final class DTV {
         }
 
         void execute(DTV dtv, String strValue) throws SQLServerException {
-            tdsWriter.writeRPCStringUnicode(name, strValue, isOutParam, collation, dtv.isNonPLP);
+            if (dtv.getJdbcType() == JDBCType.GUID) {
+                tdsWriter.writeRPCUUID(name, UUID.fromString(strValue), isOutParam);
+            } else {
+                tdsWriter.writeRPCStringUnicode(name, strValue, isOutParam, collation, dtv.isNonPLP);
+            }
         }
 
         void execute(DTV dtv, Clob clobValue) throws SQLServerException {
@@ -1126,6 +1132,10 @@ final class DTV {
             tdsWriter.writeRPCBit(name, booleanValue, isOutParam);
         }
 
+        void execute(DTV dtv, UUID uuidValue) throws SQLServerException {
+            tdsWriter.writeRPCUUID(name, uuidValue, isOutParam);
+        }
+
         void execute(DTV dtv, byte[] byteArrayValue) throws SQLServerException {
             if (null != cryptoMeta) {
                 tdsWriter.writeRPCNameValType(name, isOutParam, TDSType.BIGVARBINARY);
@@ -1537,10 +1547,13 @@ final class DTV {
                 case VARCHAR:
                 case LONGVARCHAR:
                 case CLOB:
-                case GUID:
                     op.execute(this, (byte[]) null);
                     break;
 
+                case GUID:
+                    op.execute(this, (UUID) null);
+                    break;
+
                 case TINYINT:
                     if (null != cryptoMeta)
                         op.execute(this, (byte[]) null);
@@ -1619,7 +1632,7 @@ final class DTV {
                             byte[] bArray = Util.asGuidByteArray((UUID) value);
                             op.execute(this, bArray);
                         } else {
-                            op.execute(this, String.valueOf(value));
+                            op.execute(this, UUID.fromString(String.valueOf(value)));
                         }
                     } else if (JDBCType.SQL_VARIANT == jdbcType) {
                         op.execute(this, String.valueOf(value));
@@ -2194,6 +2207,8 @@ final class AppDTVImpl extends DTVImpl {
 
         void execute(DTV dtv, Boolean booleanValue) throws SQLServerException {}
 
+        void execute(DTV dtv, UUID uuidValue) throws SQLServerException {}
+
         void execute(DTV dtv, byte[] byteArrayValue) throws SQLServerException {}
 
         void execute(DTV dtv, Blob blobValue) throws SQLServerException {
diff --git a/src/main/java/microsoft/sql/DateTimeOffset.java b/src/main/java/microsoft/sql/DateTimeOffset.java
index e2fcc840..bf9e95c7 100644
--- a/src/main/java/microsoft/sql/DateTimeOffset.java
+++ b/src/main/java/microsoft/sql/DateTimeOffset.java
@@ -72,6 +72,22 @@ public final class DateTimeOffset implements java.io.Serializable, java.lang.Com
         assert 0 == this.utcMillis % 1000L : "utcMillis: " + this.utcMillis;
     }
 
+    /**
+     * Constructs a DateTimeOffset from an existing java.time.OffsetDateTime
+     * DateTimeOffset represents values to 100 nanosecond precision. If the java.time.OffsetDateTime instance
+     * represents a value that is more precise, the value is rounded to the nearest multiple of 100 nanoseconds. Values
+     * within 50 nanoseconds of the next second are rounded up to the next second.
+     *
+     * @param offsetDateTime
+     *        A java.time.OffsetDateTime value
+     */
+    private DateTimeOffset(java.time.OffsetDateTime offsetDateTime) {
+        int hundredNanos = ((offsetDateTime.getNano() + 50) / 100);
+        this.utcMillis = (offsetDateTime.toEpochSecond() * 1000) + (hundredNanos / HUNDRED_NANOS_PER_SECOND * 1000);
+        this.nanos = 100 * (hundredNanos % HUNDRED_NANOS_PER_SECOND);
+        this.minutesOffset = offsetDateTime.getOffset().getTotalSeconds() / 60;
+    }
+
     /**
      * Converts a java.sql.Timestamp value with an integer offset to the equivalent DateTimeOffset value
      * 
@@ -105,6 +121,20 @@ public final class DateTimeOffset implements java.io.Serializable, java.lang.Com
                 (calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET)) / (60 * 1000));
     }
 
+    /**
+     * Directly converts a {@link java.time.OffsetDateTime} value to an equivalent {@link DateTimeOffset} value
+     * DateTimeOffset represents values to 100 nanosecond precision. If the java.time.OffsetDateTime instance
+     * represents a value that is more precise, the value is rounded to the nearest multiple of 100 nanoseconds. Values
+     * within 50 nanoseconds of the next second are rounded up to the next second.
+     *
+     * @param offsetDateTime
+     *        A java.time.OffsetDateTime value
+     * @return The DateTimeOffset value of the input java.time.OffsetDateTime
+     */
+    public static DateTimeOffset valueOf(java.time.OffsetDateTime offsetDateTime) {
+        return new DateTimeOffset(offsetDateTime);
+    }
+
     /** formatted value */
     private String formattedValue = null;
 


Test patch:
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java
index eabbbb57..12eea46c 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java
@@ -53,6 +53,7 @@ public class SQLServerConnectionTest extends AbstractTest {
     // If no retry is done, the function should at least exit in 5 seconds
     static int threshHoldForNoRetryInMilliseconds = 5000;
     static int loginTimeOutInSeconds = 10;
+    static String tnirHost = getConfiguredProperty("tnirHost");
 
     String randomServer = RandomUtil.getIdentifier("Server");
 
@@ -489,6 +490,44 @@ public class SQLServerConnectionTest extends AbstractTest {
         }
     }
 
+    // Test connect retry 0 but should still connect to TNIR
+    @Test
+    @Tag(Constants.xAzureSQLDW)
+    @Tag(Constants.xAzureSQLDB)
+    @Tag(Constants.reqExternalSetup)
+    public void testConnectTnir() {
+        org.junit.Assume.assumeTrue(isWindows);
+
+        // no retries but should connect to TNIR (this assumes host is defined in host file
+        try (Connection con = PrepUtil
+                .getConnection(connectionString + ";transparentNetworkIPResolution=true;connectRetryCount=0;serverName="
+                        + tnirHost);) {} catch (Exception e) {
+            fail(e.getMessage());
+        }
+    }
+
+    // Test connect retry 0 and TNIR disabled
+    @Test
+    @Tag(Constants.xAzureSQLDW)
+    @Tag(Constants.xAzureSQLDB)
+    @Tag(Constants.reqExternalSetup)
+    public void testConnectNoTnir() {
+        org.junit.Assume.assumeTrue(isWindows);
+
+        // no retries no TNIR should fail even tho host is defined in host file
+        try (Connection con = PrepUtil.getConnection(connectionString
+                + ";transparentNetworkIPResolution=false;connectRetryCount=0;serverName=" + tnirHost);) {
+            assertTrue(con == null, TestResource.getResource("R_shouldNotConnect"));
+        } catch (Exception e) {
+            assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_tcpipConnectionFailed"))
+                    || ((isSqlAzure() || isSqlAzureDW())
+                                                         ? e.getMessage().contains(
+                                                                 TestResource.getResource("R_connectTimedOut"))
+                                                         : false),
+                    e.getMessage());
+        }
+    }
+
     @Test
     @Tag(Constants.xAzureSQLDW)
     @Tag(Constants.xAzureSQLDB)
@@ -981,7 +1020,9 @@ public class SQLServerConnectionTest extends AbstractTest {
 
                 ds.setURL(connectionString);
                 ds.setServerName("invalidServerName" + UUID.randomUUID());
-                ds.setLoginTimeout(5);
+                ds.setLoginTimeout(30);
+                ds.setConnectRetryCount(3);
+                ds.setConnectRetryInterval(10);
                 try (Connection con = ds.getConnection()) {} catch (SQLException e) {}
             }
         };
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java
index e14bb671..aa4252f9 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java
@@ -63,6 +63,11 @@ public final class TestResource extends ListResourceBundle {
             {"R_ConnectionURLNull", "The connection URL is null."},
             {"R_connectionIsNotClosed", "The connection is not closed."},
             {"R_invalidExceptionMessage", "Invalid exception message"},
+            {"R_invalidClientSecret", "AADSTS7000215: Invalid client secret provided"},
+            {"R_invalidCertFields",
+                    "Error reading certificate, please verify the location of the certificate.signed fields invalid"},
+            {"R_invalidAADAuth",
+                    "Failed to authenticate the user {0} in Active Directory (Authentication={1})"},
             {"R_failedValidate", "failed to validate values in $0} "}, {"R_tableNotDropped", "table not dropped. "},
             {"R_connectionReset", "Connection reset"}, {"R_unknownException", "Unknown exception"},
             {"R_deadConnection", "Dead connection should be invalid"},
@@ -212,5 +217,7 @@ public final class TestResource extends ListResourceBundle {
             {"R_failedFedauth", "Failed to acquire fedauth token: "},
             {"R_noLoginModulesConfiguredForJdbcDriver",
                     "javax.security.auth.login.LoginException (No LoginModules configured for SQLJDBCDriver)"},
-            {"R_unexpectedThreadCount", "Thread count is higher than expected."}};
+            {"R_unexpectedThreadCount", "Thread count is higher than expected."},
+            {"R_expectedClassDoesNotMatchActualClass",
+                    "Expected column class {0} does not match actual column class {1} for column {2}."}};
 }
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java
index 3a480548..51203939 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java
@@ -81,7 +81,7 @@ public class CallableStatementTest extends AbstractTest {
 
     /**
      * Setup before test
-     * 
+     *
      * @throws SQLException
      */
     @BeforeAll
@@ -201,7 +201,7 @@ public class CallableStatementTest extends AbstractTest {
 
     /**
      * Tests CallableStatement.getString() with uniqueidentifier parameter
-     * 
+     *
      * @throws SQLException
      */
     @Test
@@ -226,7 +226,7 @@ public class CallableStatementTest extends AbstractTest {
 
     /**
      * test for setNull(index, varchar) to behave as setNull(index, nvarchar) when SendStringParametersAsUnicode is true
-     * 
+     *
      * @throws SQLException
      */
     @Test
@@ -302,7 +302,7 @@ public class CallableStatementTest extends AbstractTest {
 
     /**
      * Tests getObject(n, java.time.OffsetDateTime.class) and getObject(n, java.time.OffsetTime.class).
-     * 
+     *
      * @throws SQLException
      */
     @Test
@@ -332,7 +332,7 @@ public class CallableStatementTest extends AbstractTest {
 
     /**
      * recognize parameter names with and without leading '@'
-     * 
+     *
      * @throws SQLException
      */
     @Test
@@ -1067,9 +1067,152 @@ public class CallableStatementTest extends AbstractTest {
         }
     }
 
+    @Test
+    public void testExecuteSystemStoredProcedureNamedParametersAndIndexedParameterNoResultset() throws SQLException {
+        String call0 = "EXEC sp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'";
+        String call1 = "\rEXEC\r\rsp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'";
+        String call2 = "  EXEC   sp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'";
+        String call3 = "\tEXEC\t\t\tsp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'";
+
+        try (CallableStatement cstmt0 = connection.prepareCall(call0);
+                CallableStatement cstmt1 = connection.prepareCall(call1);
+                CallableStatement cstmt2 = connection.prepareCall(call2);
+                CallableStatement cstmt3 = connection.prepareCall(call3);) {
+            cstmt0.setString(1, "Resource-" + UUID.randomUUID());
+            cstmt0.execute();
+
+            cstmt1.setString(1, "Resource-" + UUID.randomUUID());
+            cstmt1.execute();
+
+            cstmt2.setString(1, "Resource-" + UUID.randomUUID());
+            cstmt2.execute();
+
+            cstmt3.setString(1, "Resource-" + UUID.randomUUID());
+            cstmt3.execute();
+        }
+    }
+
+    @Test
+    public void testExecSystemStoredProcedureNamedParametersAndIndexedParameterResultSet() throws SQLException {
+        String call = "exec sp_sproc_columns_100 ?, @ODBCVer=3, @fUsePattern=0";
+
+        try (CallableStatement cstmt = connection.prepareCall(call)) {
+            cstmt.setString(1, "sp_getapplock");
+
+            try (ResultSet rs = cstmt.executeQuery()) {
+                while (rs.next()) {
+                    assertTrue(TestResource.getResource("R_resultSetEmpty"), !rs.getString(4).isEmpty());
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testExecSystemStoredProcedureNoIndexedParametersResultSet() throws SQLException {
+        String call = "execute sp_sproc_columns_100 sp_getapplock, @ODBCVer=3, @fUsePattern=0";
+
+        try (CallableStatement cstmt = connection.prepareCall(call); ResultSet rs = cstmt.executeQuery()) {
+            while (rs.next()) {
+                assertTrue(TestResource.getResource("R_resultSetEmpty"), !rs.getString(4).isEmpty());
+            }
+        }
+    }
+
+    @Test
+    public void testExecDocumentedSystemStoredProceduresIndexedParameters() throws SQLException {
+        String serverName;
+        String testTableName = "testTable";
+        Integer integer = new Integer(1);
+
+        try (Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery("SELECT @@SERVERNAME")) {
+            rs.next();
+            serverName = rs.getString(1);
+        }
+
+        String[] sprocs = {"EXEC sp_column_privileges ?", "exec sp_catalogs ?", "execute sp_column_privileges ?",
+                "EXEC sp_column_privileges_ex ?", "EXECUTE sp_columns ?", "execute sp_datatype_info ?",
+                "EXEC sp_sproc_columns ?", "EXECUTE sp_server_info ?", "exec sp_special_columns ?",
+                "execute sp_statistics ?", "EXEC sp_table_privileges ?", "exec sp_tables ?"};
+
+        Object[] params = {testTableName, serverName, testTableName, serverName, testTableName, integer,
+                "sp_column_privileges", integer, testTableName, testTableName, testTableName, testTableName};
+
+        int paramIndex = 0;
+
+        for (String sproc : sprocs) {
+            try (CallableStatement cstmt = connection.prepareCall(sproc)) {
+                cstmt.setObject(1, params[paramIndex]);
+                cstmt.execute();
+                paramIndex++;
+            } catch (Exception e) {
+                fail("Failed executing '" + sproc + "' with indexed parameter '" + params[paramIndex]);
+            }
+        }
+    }
+
+    @Test
+    @Tag(Constants.reqExternalSetup)
+    @Tag(Constants.xAzureSQLDB)
+    @Tag(Constants.xAzureSQLDW)
+    @Tag(Constants.xAzureSQLMI)
+    public void testFourPartSyntaxCallEscapeSyntax() throws SQLException {
+        String table = "serverList";
+
+        try (Statement stmt = connection.createStatement()) {
+            stmt.execute("IF OBJECT_ID(N'" + table + "') IS NOT NULL DROP TABLE " + table);
+            stmt.execute("CREATE TABLE " + table + " (serverName varchar(100),network varchar(100),serverStatus varchar(4000), id int, collation varchar(100), connectTimeout int, queryTimeout int)");
+            stmt.execute("INSERT " + table + " EXEC sp_helpserver");
+
+            ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM " + table + " WHERE serverName = N'" + linkedServer + "'");
+            rs.next();
+
+            if (rs.getInt(1) == 1) {
+                stmt.execute("EXEC sp_dropserver @server='" + linkedServer + "';");
+            }
+
+            stmt.execute("EXEC sp_addlinkedserver @server='" + linkedServer + "';");
+            stmt.execute("EXEC sp_addlinkedsrvlogin @rmtsrvname=N'" + linkedServer + "', @rmtuser=N'" + remoteUser + "', @rmtpassword=N'" + remotePassword + "'");
+            stmt.execute("EXEC sp_serveroption '" + linkedServer + "', 'rpc', true;");
+            stmt.execute("EXEC sp_serveroption '" + linkedServer + "', 'rpc out', true;");
+        }
+
+        SQLServerDataSource ds = new SQLServerDataSource();
+        ds.setServerName(linkedServer);
+        ds.setUser(remoteUser);
+        ds.setPassword(remotePassword);
+        ds.setEncrypt(false);
+        ds.setTrustServerCertificate(true);
+
+        try (Connection linkedServerConnection = ds.getConnection(); Statement stmt = linkedServerConnection.createStatement()) {
+            stmt.execute("create or alter procedure dbo.TestAdd(@Num1 int, @Num2 int, @Result int output) as begin set @Result = @Num1 + @Num2; end;");
+        }
+
+        try (CallableStatement cstmt = connection.prepareCall("{call [" + linkedServer + "].master.dbo.TestAdd(?,?,?)}")) {
+            int sum = 11;
+            int param0 = 1;
+            int param1 = 10;
+            cstmt.setInt(1, param0);
+            cstmt.setInt(2, param1);
+            cstmt.registerOutParameter(3, Types.INTEGER);
+            cstmt.execute();
+            assertEquals(sum, cstmt.getInt(3));
+        }
+
+        try (CallableStatement cstmt = connection.prepareCall("exec [" + linkedServer + "].master.dbo.TestAdd ?,?,?")) {
+            int sum = 11;
+            int param0 = 1;
+            int param1 = 10;
+            cstmt.setInt(1, param0);
+            cstmt.setInt(2, param1);
+            cstmt.registerOutParameter(3, Types.INTEGER);
+            cstmt.execute();
+            assertEquals(sum, cstmt.getInt(3));
+        }
+    }
+
     /**
      * Cleanup after test
-     * 
+     *
      * @throws SQLException
      */
     @AfterAll
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java
index af2bda8a..68fbfc8d 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java
@@ -9,7 +9,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
 import java.lang.reflect.Field;
-import java.lang.reflect.Method;
 import java.sql.Connection;
 import java.sql.DriverManager;
 import java.sql.SQLException;
@@ -36,67 +35,81 @@ import com.microsoft.sqlserver.testframework.PrepUtil;
 
 
 @RunWith(JUnitPlatform.class)
-@Tag("slow")
 public class TimeoutTest extends AbstractTest {
     static String randomServer = RandomUtil.getIdentifier("Server");
     static String waitForDelaySPName = RandomUtil.getIdentifier("waitForDelaySP");
     static final int waitForDelaySeconds = 10;
-    static final int defaultTimeout = 15; // loginTimeout default value
+    static final int defaultTimeout = 30; // loginTimeout default value
 
     @BeforeAll
     public static void setupTests() throws Exception {
         setConnection();
     }
 
+    /*
+     * TODO:
+     * The tests below uses a simple interval counting logic to determine whether there was at least 1 retry.
+     * Given the interval is long enough, then 1 retry should take at least 1 interval long, so if it took < 1 interval, then it assumes there were no retry. However, this only works if TNIR or failover is not enabled since those cases should retry but no wait interval in between. So this interval counting can not detect these cases.
+     * Note a better and more reliable way would be to check attemptNumber using reflection to determine the number of retries.
+     */
+
+    // test default loginTimeout used if not specified in connection string
     @Test
     public void testDefaultLoginTimeout() {
-        long timerEnd = 0;
-
+        long totalTime = 0;
         long timerStart = System.currentTimeMillis();
-        // Try a non existing server and see if the default timeout is 15 seconds
-        try (Connection con = PrepUtil.getConnection("jdbc:sqlserver://" + randomServer + "connectRetryCount=0")) {
+
+        // non existing server and default values to see if took default timeout
+        try (Connection con = PrepUtil.getConnection("jdbc:sqlserver://" + randomServer)) {
             fail(TestResource.getResource("R_shouldNotConnect"));
         } catch (Exception e) {
-            timerEnd = System.currentTimeMillis();
+            totalTime = System.currentTimeMillis() - timerStart;
 
-            assertTrue((e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost")))
-                    || ((isSqlAzure() || isSqlAzureDW())
-                                                         ? e.getMessage().contains(
-                                                                 TestResource.getResource("R_connectTimedOut"))
-                                                         : false),
+            assertTrue(
+                    (e.getMessage().toLowerCase()
+                            .contains(TestResource.getResource("R_tcpipConnectionToHost").toLowerCase()))
+                            || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase()
+                                    .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false),
                     e.getMessage());
         }
 
-        verifyTimeout(timerEnd - timerStart, defaultTimeout);
+        // time should be < default loginTimeout
+        assertTrue(totalTime < TimeUnit.SECONDS.toMillis(defaultTimeout),
+                "total time: " + totalTime + " default loginTimout: " + TimeUnit.SECONDS.toMillis(defaultTimeout));
     }
 
+    // test setting loginTimeout value
     @Test
     public void testURLLoginTimeout() {
-        long timerEnd = 0;
-        int timeout = 10;
+        long totalTime = 0;
+        int timeout = 15;
 
         long timerStart = System.currentTimeMillis();
 
+        // non existing server and set loginTimeout
         try (Connection con = PrepUtil.getConnection("jdbc:sqlserver://" + randomServer + ";logintimeout=" + timeout)) {
             fail(TestResource.getResource("R_shouldNotConnect"));
         } catch (Exception e) {
-            timerEnd = System.currentTimeMillis();
+            totalTime = System.currentTimeMillis() - timerStart;
 
-            assertTrue((e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost")))
-                    || ((isSqlAzure() || isSqlAzureDW())
-                                                         ? e.getMessage().contains(
-                                                                 TestResource.getResource("R_connectTimedOut"))
-                                                         : false),
+            assertTrue(
+                    (e.getMessage().toLowerCase()
+                            .contains(TestResource.getResource("R_tcpipConnectionToHost").toLowerCase()))
+                            || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase()
+                                    .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false),
                     e.getMessage());
         }
 
-        verifyTimeout(timerEnd - timerStart, timeout);
+        // time should be < set loginTimeout
+        assertTrue(totalTime < TimeUnit.SECONDS.toMillis(timeout),
+                "total time: " + totalTime + " loginTimeout: " + TimeUnit.SECONDS.toMillis(timeout));
     }
 
+    // test setting timeout in DM
     @Test
     public void testDMLoginTimeoutApplied() {
-        long timerEnd = 0;
-        int timeout = 10;
+        long totalTime = 0;
+        int timeout = 15;
 
         DriverManager.setLoginTimeout(timeout);
         long timerStart = System.currentTimeMillis();
@@ -104,23 +117,26 @@ public class TimeoutTest extends AbstractTest {
         try (Connection con = PrepUtil.getConnection("jdbc:sqlserver://" + randomServer)) {
             fail(TestResource.getResource("R_shouldNotConnect"));
         } catch (Exception e) {
-            timerEnd = System.currentTimeMillis();
+            totalTime = System.currentTimeMillis() - timerStart;
 
-            assertTrue((e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost")))
-                    || ((isSqlAzure() || isSqlAzureDW())
-                                                         ? e.getMessage().contains(
-                                                                 TestResource.getResource("R_connectTimedOut"))
-                                                         : false),
+            assertTrue(
+                    (e.getMessage().toLowerCase()
+                            .contains(TestResource.getResource("R_tcpipConnectionToHost").toLowerCase()))
+                            || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase()
+                                    .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false),
                     e.getMessage());
         }
 
-        verifyTimeout(timerEnd - timerStart, timeout);
+        // time should be < DM timeout
+        assertTrue(totalTime < TimeUnit.SECONDS.toMillis(timeout),
+                "total time: " + totalTime + " DM loginTimeout: " + TimeUnit.SECONDS.toMillis(timeout));
     }
 
+    // test that setting in connection string overrides value set in DM
     @Test
     public void testDMLoginTimeoutNotApplied() {
-        long timerEnd = 0;
-        int timeout = 10;
+        long totalTime = 0;
+        int timeout = 15;
         try {
             DriverManager.setLoginTimeout(timeout * 3); // 30 seconds
             long timerStart = System.currentTimeMillis();
@@ -129,134 +145,172 @@ public class TimeoutTest extends AbstractTest {
                     .getConnection("jdbc:sqlserver://" + randomServer + ";loginTimeout=" + timeout)) {
                 fail(TestResource.getResource("R_shouldNotConnect"));
             } catch (Exception e) {
-                timerEnd = System.currentTimeMillis();
+                totalTime = System.currentTimeMillis() - timerStart;
 
                 assertTrue(
-                        (e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost")))
-                                || ((isSqlAzure() || isSqlAzureDW())
-                                                                     ? e.getMessage()
-                                                                             .contains(TestResource
-                                                                                     .getResource("R_connectTimedOut"))
-                                                                     : false),
+                        (e.getMessage().toLowerCase()
+                                .contains(TestResource.getResource("R_tcpipConnectionToHost").toLowerCase()))
+                                || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase()
+                                        .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false),
                         e.getMessage());
             }
-            verifyTimeout(timerEnd - timerStart, timeout);
+
+            // time should be < connection string loginTimeout
+            assertTrue(totalTime < TimeUnit.SECONDS.toMillis(timeout),
+                    "total time: " + totalTime + " loginTimeout: " + TimeUnit.SECONDS.toMillis(timeout));
         } finally {
             DriverManager.setLoginTimeout(0); // Default to 0 again
         }
     }
 
+    // Test connect retry set to 0 (disabled)
+    @Test
+    public void testConnectRetryDisable() {
+        long totalTime = 0;
+        long timerStart = System.currentTimeMillis();
+        int interval = defaultTimeout; // long interval so we can tell if there was a retry
+        long timeout = defaultTimeout * 2; // long loginTimeout to accommodate the long interval
+
+        // non existent server with long loginTimeout, should return fast if no retries at all
+        try (Connection con = PrepUtil.getConnection(
+                "jdbc:sqlserver://" + randomServer + ";transparentNetworkIPResolution=false;loginTimeout=" + timeout
+                        + ";connectRetryCount=0;connectInterval=" + interval)) {
+            fail(TestResource.getResource("R_shouldNotConnect"));
+        } catch (Exception e) {
+            totalTime = System.currentTimeMillis() - timerStart;
+
+            assertTrue(
+                    e.getMessage().matches(TestUtils.formatErrorMsg("R_tcpipConnectionFailed"))
+                            || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase()
+                                    .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false),
+                    e.getMessage());
+        }
+
+        // if there was a retry then it would take at least 1 interval long, so if < interval means there were no retries
+        assertTrue(totalTime < TimeUnit.SECONDS.toMillis(interval),
+                "total time: " + totalTime + " interval: " + TimeUnit.SECONDS.toMillis(interval));
+    }
+
     // Test connect retry for non-existent server with loginTimeout
     @Test
     public void testConnectRetryBadServer() {
-        long timerEnd = 0;
+        long totalTime = 0;
         long timerStart = System.currentTimeMillis();
-        int loginTimeout = 15;
+        int timeout = 15;
 
         // non existent server with very short loginTimeout, no retry will happen as not a transient error
-        try (Connection con = PrepUtil
-                .getConnection("jdbc:sqlserver://" + randomServer + ";loginTimeout=" + loginTimeout)) {
+        try (Connection con = PrepUtil.getConnection("jdbc:sqlserver://" + randomServer + ";loginTimeout=" + timeout)) {
             fail(TestResource.getResource("R_shouldNotConnect"));
         } catch (Exception e) {
-            timerEnd = System.currentTimeMillis();
+            totalTime = System.currentTimeMillis() - timerStart;
 
-            assertTrue((e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost")))
-                    || ((isSqlAzure() || isSqlAzureDW())
-                                                         ? e.getMessage().contains(
-                                                                 TestResource.getResource("R_connectTimedOut"))
-                                                         : false),
+            assertTrue(
+                    (e.getMessage().toLowerCase()
+                            .contains(TestResource.getResource("R_tcpipConnectionToHost").toLowerCase()))
+                            || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase()
+                                    .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false),
                     e.getMessage());
         }
 
-        verifyTimeout(timerEnd - timerStart, loginTimeout);
+        // time should be < loginTimeout set
+        assertTrue(totalTime < TimeUnit.SECONDS.toMillis(timeout),
+                "total time: " + totalTime + " loginTimeout: " + TimeUnit.SECONDS.toMillis(timeout));
     }
 
     // Test connect retry for database error
     @Test
     public void testConnectRetryServerError() {
-        long timerEnd = 0;
+        long totalTime = 0;
         long timerStart = System.currentTimeMillis();
+        int interval = defaultTimeout; // long interval so we can tell if there was a retry
+        long timeout = defaultTimeout * 2; // long loginTimeout to accommodate the long interval
 
-        // non existent database with interval < loginTimeout this will generate a 4060 transient error and retry
-        int connectRetryCount = new Random().nextInt(256);
-        int connectRetryInterval = new Random().nextInt(defaultTimeout) + 1;
+        // non existent database with interval < loginTimeout this will generate a 4060 transient error and retry 1 time
         try (Connection con = PrepUtil.getConnection(
                 TestUtils.addOrOverrideProperty(connectionString, "database", RandomUtil.getIdentifier("database"))
-                        + ";logintimeout=" + defaultTimeout + ";connectRetryCount=" + connectRetryCount
-                        + ";connectRetryInterval=" + connectRetryInterval)) {
+                        + ";loginTimeout=" + timeout + ";connectRetryCount=" + 1 + ";connectRetryInterval=" + interval
+                        + ";transparentNetworkIPResolution=false")) {
             fail(TestResource.getResource("R_shouldNotConnect"));
         } catch (Exception e) {
-            timerEnd = System.currentTimeMillis();
+            totalTime = System.currentTimeMillis() - timerStart;
 
-            assertTrue((e.getMessage().contains(TestResource.getResource("R_cannotOpenDatabase")))
-                    || ((isSqlAzure() || isSqlAzureDW())
-                                                         ? e.getMessage().contains(
-                                                                 TestResource.getResource("R_connectTimedOut"))
-                                                         : false),
+            assertTrue(
+                    (e.getMessage().toLowerCase()
+                            .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase()))
+                            || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase()
+                                    .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false),
                     e.getMessage());
         }
 
-        // connect + all retries should always be <= loginTimeout
-        verifyTimeout(timerEnd - timerStart, defaultTimeout);
+        // 1 retry should be at least 1 interval long but < 2 intervals
+        assertTrue(TimeUnit.SECONDS.toMillis(interval) < totalTime,
+                "interval: " + TimeUnit.SECONDS.toMillis(interval) + " total time: " + totalTime);
+        assertTrue(totalTime < TimeUnit.SECONDS.toMillis(2 * interval),
+                "total time: " + totalTime + " 2 * interval: " + TimeUnit.SECONDS.toMillis(interval));
     }
 
     // Test connect retry for database error using Datasource
     @Test
     public void testConnectRetryServerErrorDS() {
-        long timerEnd = 0;
+        long totalTime = 0;
         long timerStart = System.currentTimeMillis();
+        int interval = defaultTimeout; // long interval so we can tell if there was a retry
+        long loginTimeout = defaultTimeout * 2; // long loginTimeout to accommodate the long interval
 
-        // non existent database with interval < loginTimeout this will generate a 4060 transient error and retry
-        int connectRetryCount = new Random().nextInt(256);
-        int connectRetryInterval = new Random().nextInt(defaultTimeout) + 1;
-
+        // non existent database with interval < loginTimeout this will generate a 4060 transient error and retry 1 time
         SQLServerDataSource ds = new SQLServerDataSource();
         String connectStr = TestUtils.addOrOverrideProperty(connectionString, "database",
-                RandomUtil.getIdentifier("database")) + ";logintimeout=" + defaultTimeout + ";connectRetryCount="
-                + connectRetryCount + ";connectRetryInterval=" + connectRetryInterval;
+                RandomUtil.getIdentifier("database")) + ";logintimeout=" + loginTimeout + ";connectRetryCount=1"
+                + ";connectRetryInterval=" + interval;
         updateDataSource(connectStr, ds);
 
         try (Connection con = PrepUtil.getConnection(connectStr)) {
             fail(TestResource.getResource("R_shouldNotConnect"));
         } catch (Exception e) {
-            assertTrue((e.getMessage().contains(TestResource.getResource("R_cannotOpenDatabase")))
-                    || ((isSqlAzure() || isSqlAzureDW())
-                                                         ? e.getMessage().contains(
-                                                                 TestResource.getResource("R_connectTimedOut"))
-                                                         : false),
+            assertTrue(
+                    (e.getMessage().toLowerCase()
+                            .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase()))
+                            || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase()
+                                    .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false),
                     e.getMessage());
-            timerEnd = System.currentTimeMillis();
+            totalTime = System.currentTimeMillis() - timerStart;
         }
 
-        // connect + all retries should always be <= loginTimeout
-        verifyTimeout(timerEnd - timerStart, defaultTimeout);
+        // 1 retry should be at least 1 interval long but < 2 intervals
+        assertTrue(TimeUnit.SECONDS.toMillis(interval) < totalTime,
+                "interval: " + TimeUnit.SECONDS.toMillis(interval) + " total time: " + totalTime);
+        assertTrue(totalTime < TimeUnit.SECONDS.toMillis(2 * interval),
+                "total time: " + totalTime + " 2 * interval: " + TimeUnit.SECONDS.toMillis(2 * interval));
     }
 
     // Test connect retry for database error with loginTimeout
     @Test
     public void testConnectRetryTimeout() {
-        long timerEnd = 0;
+        long totalTime = 0;
         long timerStart = System.currentTimeMillis();
+        int interval = defaultTimeout; // long interval so we can tell if there was a retry
         int loginTimeout = 2;
 
-        // non existent database with very short loginTimeout so there is no time to do all retries
+        // non existent database with very short loginTimeout so there is no time to do any retry
         try (Connection con = PrepUtil.getConnection(
                 TestUtils.addOrOverrideProperty(connectionString, "database", RandomUtil.getIdentifier("database"))
-                        + "connectRetryCount=" + (new Random().nextInt(256)) + ";connectRetryInterval="
-                        + (new Random().nextInt(defaultTimeout - 1) + 1) + ";loginTimeout=" + loginTimeout)) {
+                        + "connectRetryCount=" + (new Random().nextInt(256)) + ";connectRetryInterval=" + interval
+                        + ";loginTimeout=" + loginTimeout)) {
             fail(TestResource.getResource("R_shouldNotConnect"));
         } catch (Exception e) {
-            timerEnd = System.currentTimeMillis();
+            totalTime = System.currentTimeMillis() - timerStart;
 
-            assertTrue((e.getMessage().contains(TestResource.getResource("R_cannotOpenDatabase")))
-                    || ((isSqlAzure() || isSqlAzureDW())
-                                                         ? e.getMessage().contains(
-                                                                 TestResource.getResource("R_connectTimedOut"))
-                                                         : false),
+            assertTrue(
+                    (e.getMessage().toLowerCase()
+                            .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase()))
+                            || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase()
+                                    .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false),
                     e.getMessage());
         }
 
-        verifyTimeout(timerEnd - timerStart, loginTimeout);
+        // if there was a retry then it would take at least 1 interval long, so if < interval means there were no retries
+        assertTrue(totalTime < TimeUnit.SECONDS.toMillis(interval),
+                "total time: " + totalTime + " interval: " + TimeUnit.SECONDS.toMillis(interval));
     }
 
     // Test for detecting Azure server for connection retries
@@ -289,80 +343,6 @@ public class TimeoutTest extends AbstractTest {
         }
     }
 
-    @Test
-    public void testFailoverInstanceResolution() throws SQLException {
-        long timerEnd = 0;
-        long timerStart = System.currentTimeMillis();
-
-        // Try a non existing server and see if the default timeout is 15 seconds
-        try (Connection con = PrepUtil
-                .getConnection("jdbc:sqlserver://" + randomServer + ";databaseName=FailoverDB_abc;failoverPartner="
-                        + randomServer + "\\foo;user=sa;password=" + RandomUtil.getIdentifier("password"))) {
-            fail(TestResource.getResource("R_shouldNotConnect"));
-        } catch (Exception e) {
-            timerEnd = System.currentTimeMillis();
-
-            assertTrue((e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost")))
-                    || ((isSqlAzure() || isSqlAzureDW())
-                                                         ? e.getMessage().contains(
-                                                                 TestResource.getResource("R_connectTimedOut"))
-                                                         : false),
-                    e.getMessage());
-        }
-
-        verifyTimeout(timerEnd - timerStart, defaultTimeout * 2);
-    }
-
-    @Test
-    public void testFOInstanceResolution2() throws SQLException {
-        long timerEnd = 0;
-
-        long timerStart = System.currentTimeMillis();
-        try (Connection con = PrepUtil
-                .getConnection("jdbc:sqlserver://" + randomServer + "\\fooggg;databaseName=FailoverDB;failoverPartner="
-                        + randomServer + "\\foo;user=sa;password=" + RandomUtil.getIdentifier("password"))) {
-            fail(TestResource.getResource("R_shouldNotConnect"));
-        } catch (Exception e) {
-            timerEnd = System.currentTimeMillis();
-        }
-
-        verifyTimeout(timerEnd - timerStart, defaultTimeout);
-    }
-
-    /**
-     * Tests that failover is correctly used after a socket timeout, by confirming total time includes socketTimeout
-     * for both primary and failover server.
-     */
-    @Test
-    public void testFailoverInstanceResolutionWithSocketTimeout() {
-        long timerEnd;
-        long timerStart = System.currentTimeMillis();
-
-        try (Connection con = PrepUtil.getConnection("jdbc:sqlserver://" + randomServer
-                + ";databaseName=FailoverDB;failoverPartner=" + randomServer + "\\foo;user=sa;password="
-                + RandomUtil.getIdentifier("password") + ";socketTimeout=" + waitForDelaySeconds)) {
-            fail(TestResource.getResource("R_shouldNotConnect"));
-        } catch (Exception e) {
-            timerEnd = System.currentTimeMillis();
-            if (!(e instanceof SQLException)) {
-                fail(TestResource.getResource("R_unexpectedErrorMessage") + e.getMessage());
-            }
-
-            // Driver should correctly attempt to connect to db, experience a socketTimeout, attempt to connect to
-            // failover, and then have another socketTimeout. So, expected total time is 2 x socketTimeout.
-            long totalTime = timerEnd - timerStart;
-            long totalExpectedTime = waitForDelaySeconds * 1000L * 2; // We expect 2 * socketTimeout
-            assertTrue(totalTime >= totalExpectedTime, TestResource.getResource("R_executionNotLong") + "totalTime: "
-                    + totalTime + " expectedTime: " + totalExpectedTime);
-        }
-    }
-
-    private void verifyTimeout(long timeDiff, int timeout) {
-        // Verify that login timeout does not take longer than <timeout * 2> seconds.
-        assertTrue(timeDiff < TimeUnit.SECONDS.toMillis(timeout * 2),
-                "timeout: " + TimeUnit.SECONDS.toMillis(timeout) + " timediff: " + timeDiff);
-    }
-
     /**
      * When query timeout occurs, the connection is still usable.
      * 
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java
index ff2f2fc7..999ccdc1 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java
@@ -63,6 +63,38 @@ public class DatabaseMetaDataTest extends AbstractTest {
     private static final String functionName = RandomUtil.getIdentifier("DBMetadataFunction");
     private static Map<Integer, String> getColumnsDWColumns = null;
     private static Map<Integer, String> getImportedKeysDWColumns = null;
+    private static final String TABLE_CAT = "TABLE_CAT";
+    private static final String TABLE_SCHEM = "TABLE_SCHEM";
+    private static final String TABLE_NAME = "TABLE_NAME";
+    private static final String COLUMN_NAME = "COLUMN_NAME";
+    private static final String DATA_TYPE = "DATA_TYPE";
+    private static final String TYPE_NAME = "TYPE_NAME";
+    private static final String COLUMN_SIZE = "COLUMN_SIZE";
+    private static final String BUFFER_LENGTH = "BUFFER_LENGTH";
+    private static final String DECIMAL_DIGITS = "DECIMAL_DIGITS";
+    private static final String NUM_PREC_RADIX = "NUM_PREC_RADIX";
+    private static final String NULLABLE = "NULLABLE";
+    private static final String REMARKS = "REMARKS";
+    private static final String COLUMN_DEF = "COLUMN_DEF";
+    private static final String SQL_DATA_TYPE = "SQL_DATA_TYPE";
+    private static final String SQL_DATETIME_SUB = "SQL_DATETIME_SUB";
+    private static final String CHAR_OCTET_LENGTH = "CHAR_OCTET_LENGTH";
+    private static final String ORDINAL_POSITION = "ORDINAL_POSITION";
+    private static final String IS_NULLABLE = "IS_NULLABLE";
+    private static final String SCOPE_CATALOG = "SCOPE_CATALOG";
+    private static final String SCOPE_SCHEMA = "SCOPE_SCHEMA";
+    private static final String SCOPE_TABLE = "SCOPE_TABLE";
+    private static final String SOURCE_DATA_TYPE = "SOURCE_DATA_TYPE";
+    private static final String IS_AUTOINCREMENT = "IS_AUTOINCREMENT";
+    private static final String IS_GENERATEDCOLUMN = "IS_GENERATEDCOLUMN";
+    private static final String SS_IS_SPARSE = "SS_IS_SPARSE";
+    private static final String SS_IS_COLUMN_SET = "SS_IS_COLUMN_SET";
+    private static final String SS_UDT_CATALOG_NAME = "SS_UDT_CATALOG_NAME";
+    private static final String SS_UDT_SCHEMA_NAME = "SS_UDT_SCHEMA_NAME";
+    private static final String SS_UDT_ASSEMBLY_TYPE_NAME = "SS_UDT_ASSEMBLY_TYPE_NAME";
+    private static final String SS_XML_SCHEMACOLLECTION_CATALOG_NAME = "SS_XML_SCHEMACOLLECTION_CATALOG_NAME";
+    private static final String SS_XML_SCHEMACOLLECTION_SCHEMA_NAME = "SS_XML_SCHEMACOLLECTION_SCHEMA_NAME";
+    private static final String SS_XML_SCHEMACOLLECTION_NAME = "SS_XML_SCHEMACOLLECTION_NAME";
 
     /**
      * Verify DatabaseMetaData#isWrapperFor and DatabaseMetaData#unwrap.
@@ -887,6 +919,78 @@ public class DatabaseMetaDataTest extends AbstractTest {
         }
     }
 
+    /**
+     * Validates the metadata data types defined by JDBC spec.
+     * Refer to <a href="https://docs.oracle.com/javase/8/docs/api/java/sql/DatabaseMetaData.html#getColumns-java.lang.String-java.lang.String-java.lang.String-java.lang.String-">DatabaseMetadata getColumns() specs</a>
+     * 
+     * @throws SQLException
+     */
+    @Test
+    public void testValidateColumnMetadata() throws SQLException {
+        Map<String, Class<?>> getColumnMetaDataClass = new LinkedHashMap<>();
+
+        getColumnMetaDataClass.put(TABLE_CAT, String.class);
+        getColumnMetaDataClass.put(TABLE_SCHEM, String.class);
+        getColumnMetaDataClass.put(TABLE_NAME, String.class);
+        getColumnMetaDataClass.put(COLUMN_NAME, String.class);
+        getColumnMetaDataClass.put(DATA_TYPE, Integer.class);
+        getColumnMetaDataClass.put(TYPE_NAME, String.class);
+        getColumnMetaDataClass.put(COLUMN_SIZE, Integer.class);
+        getColumnMetaDataClass.put(BUFFER_LENGTH, Integer.class); // Not used
+        getColumnMetaDataClass.put(DECIMAL_DIGITS, Integer.class);
+        getColumnMetaDataClass.put(NUM_PREC_RADIX, Integer.class);
+        getColumnMetaDataClass.put(NULLABLE, Integer.class);
+        getColumnMetaDataClass.put(REMARKS, String.class);
+        getColumnMetaDataClass.put(COLUMN_DEF, String.class);
+        getColumnMetaDataClass.put(SQL_DATA_TYPE, Integer.class);
+        getColumnMetaDataClass.put(SQL_DATETIME_SUB, Integer.class);
+        getColumnMetaDataClass.put(CHAR_OCTET_LENGTH, Integer.class);
+        getColumnMetaDataClass.put(ORDINAL_POSITION, Integer.class);
+        getColumnMetaDataClass.put(IS_NULLABLE, String.class);
+        getColumnMetaDataClass.put(SCOPE_CATALOG, String.class);
+        getColumnMetaDataClass.put(SCOPE_SCHEMA, String.class);
+        getColumnMetaDataClass.put(SCOPE_TABLE, String.class);
+        getColumnMetaDataClass.put(SOURCE_DATA_TYPE, Short.class);
+        getColumnMetaDataClass.put(IS_AUTOINCREMENT, String.class);
+        getColumnMetaDataClass.put(IS_GENERATEDCOLUMN, String.class);
+        getColumnMetaDataClass.put(SS_IS_SPARSE, Short.class);
+        getColumnMetaDataClass.put(SS_IS_COLUMN_SET, Short.class);
+        getColumnMetaDataClass.put(SS_UDT_CATALOG_NAME, String.class);
+        getColumnMetaDataClass.put(SS_UDT_SCHEMA_NAME, String.class);
+        getColumnMetaDataClass.put(SS_UDT_ASSEMBLY_TYPE_NAME, String.class);
+        getColumnMetaDataClass.put(SS_XML_SCHEMACOLLECTION_CATALOG_NAME, String.class);
+        getColumnMetaDataClass.put(SS_XML_SCHEMACOLLECTION_SCHEMA_NAME, String.class);
+        getColumnMetaDataClass.put(SS_XML_SCHEMACOLLECTION_NAME, String.class);
+
+        try (Connection conn = getConnection()) {
+            ResultSetMetaData metadata = conn.getMetaData().getColumns(null, null, tableName, null).getMetaData();
+
+            // Ensure that there is an expected class for every column in the metadata result set
+            assertEquals(metadata.getColumnCount(), getColumnMetaDataClass.size());
+
+            for (int i = 1; i < metadata.getColumnCount(); i++) {
+                String columnLabel = metadata.getColumnLabel(i);
+                String columnClassName = metadata.getColumnClassName(i);
+                Class<?> expectedClass = getColumnMetaDataClass.get(columnLabel);
+
+                // Ensure the metadata column is in the metadata column class map
+                if (expectedClass == null) {
+                    MessageFormat form1 = new MessageFormat(TestResource.getResource("R_objectNullOrEmpty"));
+                    Object[] msgArgs1 = {"expected metadata column class for column " + columnLabel};
+                    fail(form1.format(msgArgs1));
+                }
+
+                // Ensure the actual and expected column metadata types match
+                if (!columnClassName.equals(expectedClass.getName())) {
+                    MessageFormat form1 = new MessageFormat(
+                            TestResource.getResource("R_expectedClassDoesNotMatchActualClass"));
+                    Object[] msgArgs1 = {expectedClass.getName(), columnClassName, columnLabel};
+                    fail(form1.format(msgArgs1));
+                }
+            }
+        }
+    }
+
     @BeforeAll
     public static void setupTable() throws Exception {
         setConnection();
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/DataTypesTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/DataTypesTest.java
index 350f7cfc..445d23dd 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/DataTypesTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/DataTypesTest.java
@@ -1935,6 +1935,17 @@ public class DataTypesTest extends AbstractTest {
         }
     }
 
+    @Test
+    public void testDateTimeOffsetValueOfOffsetDateTime() throws Exception {
+        OffsetDateTime expected = OffsetDateTime.now().withSecond(58).withNano(0);
+        OffsetDateTime roundUp = expected.withSecond(57).withNano(999999950);
+        OffsetDateTime roundDown = expected.withSecond(58).withNano(49);
+
+        assertEquals(expected, DateTimeOffset.valueOf(expected).getOffsetDateTime());
+        assertEquals(expected, DateTimeOffset.valueOf(roundUp).getOffsetDateTime());
+        assertEquals(expected, DateTimeOffset.valueOf(roundDown).getOffsetDateTime());
+    }
+
     static LocalDateTime getUnstorableValue() throws Exception {
         ZoneId systemTimezone = ZoneId.systemDefault();
         Instant now = Instant.now();
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/GuidTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/GuidTest.java
new file mode 100644
index 00000000..e8b5c157
--- /dev/null
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/GuidTest.java
@@ -0,0 +1,97 @@
+package com.microsoft.sqlserver.jdbc.datatypes;
+
+import com.microsoft.sqlserver.jdbc.RandomUtil;
+import com.microsoft.sqlserver.jdbc.SQLServerResultSet;
+import com.microsoft.sqlserver.jdbc.TestResource;
+import com.microsoft.sqlserver.jdbc.TestUtils;
+import com.microsoft.sqlserver.testframework.AbstractSQLGenerator;
+import com.microsoft.sqlserver.testframework.AbstractTest;
+import microsoft.sql.Types;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.runner.JUnitPlatform;
+import org.junit.runner.RunWith;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.Statement;
+import java.util.UUID;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+/*
+ * This test is for testing the serialisation of String as microsoft.sql.Types.GUID
+ */
+@RunWith(JUnitPlatform.class)
+public class GuidTest extends AbstractTest {
+
+    final static String tableName = RandomUtil.getIdentifier("GuidTestTable");
+    final static String escapedTableName = AbstractSQLGenerator.escapeIdentifier(tableName);
+
+    @BeforeAll
+    public static void setupTests() throws Exception {
+        setConnection();
+    }
+
+    /*
+     * Test UUID conversions
+     */
+    @Test
+    public void testGuid() throws Exception {
+        try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
+
+            // Create the test table
+            TestUtils.dropTableIfExists(escapedTableName, stmt);
+
+            String query = "create table " + escapedTableName
+                    + " (uuid uniqueidentifier, id int IDENTITY primary key)";
+            stmt.executeUpdate(query);
+
+            UUID uuid = UUID.randomUUID();
+            String uuidString = uuid.toString();
+            int id = 1;
+
+            try (PreparedStatement pstmt = conn.prepareStatement("INSERT INTO " + escapedTableName
+                    + " VALUES(?) SELECT * FROM " + escapedTableName + " where id = ?")) {
+
+                pstmt.setObject(1, uuidString, Types.GUID);
+                pstmt.setObject(2, id++);
+                pstmt.execute();
+                pstmt.getMoreResults();
+                try (SQLServerResultSet rs = (SQLServerResultSet) pstmt.getResultSet()) {
+                    rs.next();
+                    assertEquals(uuid, UUID.fromString(rs.getUniqueIdentifier(1)));
+                }
+
+                // Test NULL GUID
+                pstmt.setObject(1, null, Types.GUID);
+                pstmt.setObject(2, id++);
+                pstmt.execute();
+                pstmt.getMoreResults();
+                try (SQLServerResultSet rs = (SQLServerResultSet) pstmt.getResultSet()) {
+                    rs.next();
+                    String s = rs.getUniqueIdentifier(1);
+                    assertNull(s);
+                    assertTrue(rs.wasNull());
+                }
+
+                // Test Illegal GUID
+                try {
+                    pstmt.setObject(1, "garbage", Types.GUID);
+                    fail(TestResource.getResource("R_expectedFailPassed"));
+                } catch (IllegalArgumentException e) {
+                    assertEquals("Invalid UUID string: garbage", e.getMessage());
+                }
+            }
+        } finally {
+            try (Statement stmt = connection.createStatement()) {
+                TestUtils.dropTableIfExists(escapedTableName, stmt);
+            }
+        }
+    }
+
+}
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthTest.java
index 4bae199d..e68fcacd 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthTest.java
@@ -18,6 +18,7 @@ import java.sql.Connection;
 import java.sql.DriverManager;
 import java.sql.SQLException;
 import java.sql.Statement;
+import java.text.MessageFormat;
 import java.util.Collections;
 import java.util.Properties;
 import java.util.concurrent.CompletableFuture;
@@ -323,6 +324,66 @@ public class FedauthTest extends FedauthCommon {
         }
     }
 
+    @Test
+    public void testAADServicePrincipalAuthFailureOnSubsequentConnectionsWithInvalidatedTokenCacheWithInvalidSecret() throws Exception {
+        String url = "jdbc:sqlserver://" + azureServer + ";database=" + azureDatabase + ";authentication="
+                + SqlAuthentication.ActiveDirectoryServicePrincipal + ";Username=" + applicationClientID + ";Password="
+                + applicationKey;
+
+        String invalidSecretUrl = "jdbc:sqlserver://" + azureServer + ";database=" + azureDatabase + ";authentication="
+                + SqlAuthentication.ActiveDirectoryServicePrincipal + ";Username=" + applicationClientID + ";Password="
+                + "invalidSecret";
+
+        // Should succeed on valid secret
+        try (Connection connection = DriverManager.getConnection(url)) {}
+
+        // Should fail on invalid secret
+        try (Connection connection = DriverManager.getConnection(invalidSecretUrl)) {
+            fail(TestResource.getResource("R_expectedFailPassed"));
+        } catch (Exception e) {
+            assertTrue(e.getMessage().contains(TestResource.getResource("R_invalidClientSecret")),
+                    "Expected R_invalidClientSecret error.");
+        }
+    }
+
+    @Test
+    public void testActiveDirectoryPasswordFailureOnSubsequentConnectionsWithInvalidatedTokenCacheWithInvalidPassword() throws Exception {
+
+        // Should succeed on valid password
+        try (Connection conn = DriverManager.getConnection(adPasswordConnectionStr)) {}
+
+        // Should fail on invalid password
+        try (Connection conn = DriverManager.getConnection(adPasswordConnectionStr + ";password=invalidPassword;")) {
+            fail(TestResource.getResource("R_expectedFailPassed"));
+        } catch (Exception e) {
+            MessageFormat form = new MessageFormat(TestResource.getResource("R_invalidAADAuth"));
+            Object[] msgArgs = {azureUserName, "ActiveDirectoryPassword"};
+            assertTrue(e.getMessage().contains(form.format(msgArgs)), "Expected R_invalidAADAuth error.");
+        }
+    }
+
+    @Test
+    public void testAADServicePrincipalCertAuthFailureOnSubsequentConnectionsWithInvalidatedTokenCacheWithInvalidPassword() throws Exception {
+        // Should succeed on valid cert field values
+        String url = "jdbc:sqlserver://" + azureServer + ";database=" + azureDatabase + ";authentication="
+                + SqlAuthentication.ActiveDirectoryServicePrincipalCertificate + ";Username=" + applicationClientID
+                + ";password=" + certificatePassword + ";clientCertificate=" + clientCertificate;
+
+        // Should fail on invalid cert field values
+        String invalidPasswordUrl = "jdbc:sqlserver://" + azureServer + ";database=" + azureDatabase
+                + ";authentication=" + SqlAuthentication.ActiveDirectoryServicePrincipalCertificate + ";Username="
+                + applicationClientID + ";password=invalidPassword;clientCertificate=" + clientCertificate;
+
+        try (Connection conn = DriverManager.getConnection(url)) {}
+
+        try (Connection conn = DriverManager.getConnection(invalidPasswordUrl)) {
+            fail(TestResource.getResource("R_expectedFailPassed"));
+        } catch (Exception e) {
+            assertTrue(e.getMessage().contains(TestResource.getResource("R_invalidCertFields")),
+                    "Expected R_invalidCertFields error.");
+        }
+    }
+
     /**
      * Test invalid connection property combinations when using AAD Service Principal Authentication.
      */
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java
index 3384f278..0dace62b 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java
@@ -170,7 +170,7 @@ public class BatchExecutionTest extends AbstractTest {
     public void testValidTimezonesDstTimestampBatchInsertWithBulkCopy() throws Exception {
         Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
 
-        for (String tzId: TimeZone.getAvailableIDs()) {
+        for (String tzId : TimeZone.getAvailableIDs()) {
             TimeZone.setDefault(TimeZone.getTimeZone(tzId));
 
             long ms = 1696127400000L; // DST
@@ -191,8 +191,8 @@ public class BatchExecutionTest extends AbstractTest {
             }
 
             // Insert Timestamp using bulkcopy for batch insert
-            try (Connection con = DriverManager.getConnection(
-                    connectionString + ";useBulkCopyForBatchInsert=true;sendTemporalDataTypesAsStringForBulkCopy=false;");
+            try (Connection con = DriverManager.getConnection(connectionString
+                    + ";useBulkCopyForBatchInsert=true;sendTemporalDataTypesAsStringForBulkCopy=false;");
                     PreparedStatement pstmt = con.prepareStatement("INSERT INTO " + timestampTable1 + " VALUES(?)")) {
 
                 Timestamp timestamp = new Timestamp(ms);
@@ -235,8 +235,9 @@ public class BatchExecutionTest extends AbstractTest {
         long ms = 1578743412000L;
 
         // Insert Timestamp using prepared statement when useBulkCopyForBatchInsert=true
-        try (Connection con = DriverManager.getConnection(connectionString
-                + ";useBulkCopyForBatchInsert=true;sendTemporalDataTypesAsStringForBulkCopy=false;"); Statement stmt = con.createStatement();
+        try (Connection con = DriverManager.getConnection(
+                connectionString + ";useBulkCopyForBatchInsert=true;sendTemporalDataTypesAsStringForBulkCopy=false;");
+                Statement stmt = con.createStatement();
                 PreparedStatement pstmt = con.prepareStatement("INSERT INTO " + timestampTable2 + " VALUES(?)")) {
 
             TestUtils.dropTableIfExists(timestampTable2, stmt);
diff --git a/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java b/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java
index d17fa146..1ce9cd19 100644
--- a/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java
+++ b/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java
@@ -62,6 +62,9 @@ public abstract class AbstractTest {
     protected static String tenantID;
     protected static String[] keyIDs = null;
 
+    protected static String linkedServer = null;
+    protected static String remoteUser = null;
+    protected static String remotePassword = null;
     protected static String[] enclaveServer = null;
     protected static String[] enclaveAttestationUrl = null;
     protected static String[] enclaveAttestationProtocol = null;
@@ -197,6 +200,10 @@ public abstract class AbstractTest {
 
         clientKeyPassword = getConfiguredProperty("clientKeyPassword", "");
 
+        linkedServer = getConfiguredProperty("linkedServer", null);
+        remoteUser = getConfiguredProperty("remoteUser", null);
+        remotePassword = getConfiguredProperty("remotePassword", null);
+
         kerberosServer = getConfiguredProperty("kerberosServer", null);
         kerberosServerPort = getConfiguredProperty("kerberosServerPort", null);