Repository: microsoft/mssql-jdbc
Pull request #2361 (2024-03-21 23:10:52)

Clear prepared statement cache when resetting statement pool connection

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/microsoft/mssql-jdbc/pull/2361?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 49.67%. Comparing base [(`9a8849b`)](https://app.codecov.io/gh/microsoft/mssql-jdbc/commit/9a8849b60e74d1d01508fa61f944d9e635cab700?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft) to head [(`762fb2f`)](https://app.codecov.io/gh/microsoft/mssql-jdbc/pull/2361?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft).

> :exclamation: Current head 762fb2f differs from pull request most recent head e42426a. Consider uploading reports for the commit e42426a to get more accurate results


<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff              @@
##               main    #2361      +/-   ##
============================================
- Coverage     50.03%   49.67%   -0.37%     
+ Complexity     3785     3775      -10     
============================================
  Files           143      142       -1     
  Lines         33204    33187      -17     
  Branches       5629     5623       -6     
============================================
- Hits          16613    16484     -129     
- Misses        14207    14350     +143     
+ Partials       2384     2353      -31     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/microsoft/mssql-jdbc/pull/2361?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft).


Patch:
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ActivityCorrelator.java b/src/main/java/com/microsoft/sqlserver/jdbc/ActivityCorrelator.java
index 974ac3c4..072d34f5 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/ActivityCorrelator.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ActivityCorrelator.java
@@ -5,26 +5,32 @@
 
 package com.microsoft.sqlserver.jdbc;
 
+import java.util.Map;
 import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
 
 
 /**
- * ActivityCorrelator provides the APIs to access the ActivityId in TLS
+ * ActivityCorrelator provides the APIs to access the ActivityId in a map
  */
 final class ActivityCorrelator {
 
-    private static ThreadLocal<ActivityId> t_ActivityId = new ThreadLocal<ActivityId>() {
-        @Override
-        protected ActivityId initialValue() {
-            return new ActivityId();
-        }
-    };
+    private static Map<Long, ActivityId> activityIdMap = new ConcurrentHashMap<Long, ActivityId>();
 
     static ActivityId getCurrent() {
-        return t_ActivityId.get();
+        // get the value, not reference
+        @SuppressWarnings("deprecation")
+        long uniqueThreadId = Thread.currentThread().getId();
+
+        // Since the Id for each thread is unique, this assures that the below if statement is run only once per thread.
+        if (!activityIdMap.containsKey(uniqueThreadId)) {
+            activityIdMap.put(uniqueThreadId, new ActivityId());
+        }
+
+        return activityIdMap.get(uniqueThreadId);
     }
 
-    // Increment the Sequence number of the ActivityId in TLS
+    // Increment the Sequence number of the ActivityId
     // and return the ActivityId with new Sequence number
     static ActivityId getNext() {
         return getCurrent().getIncrement();
@@ -34,6 +40,16 @@ final class ActivityCorrelator {
      * Prevent instantiation.
      */
     private ActivityCorrelator() {}
+
+    static void cleanupActivityId() {
+        // remove the ActivityId that belongs to this thread.
+        @SuppressWarnings("deprecation")
+        long uniqueThreadId = Thread.currentThread().getId();
+
+        if (activityIdMap.containsKey(uniqueThreadId)) {
+            activityIdMap.remove(uniqueThreadId);
+        }
+    }
 }
 
 
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
index 597bc962..770cbbae 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
@@ -2367,6 +2367,10 @@ final class TDSChannel implements Serializable {
     final void setNetworkTimeout(int timeout) throws IOException {
         tcpSocket.setSoTimeout(timeout);
     }
+
+    void resetTcpSocketTimeout() throws SocketException {
+        this.tcpSocket.setSoTimeout(con.getSocketTimeoutMilliseconds());
+    }
 }
 
 
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
index 2f54a98a..2479e506 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
@@ -483,6 +483,25 @@ public interface ISQLServerConnection extends java.sql.Connection {
      */
     void setAccessTokenCallbackClass(String accessTokenCallbackClass);
 
+    /**
+     * Get Currently installed message handler on the connection
+     * 
+     * @see ISQLServerMessageHandler#messageHandler(ISQLServerMessage)
+     * @return ISQLServerMessageHandler
+     */
+    ISQLServerMessageHandler getServerMessageHandler();
+
+    /**
+     * Set message handler on the connection
+     * 
+     * @param messageHandler
+     *        message handler
+     * 
+     * @see ISQLServerMessageHandler#messageHandler(ISQLServerMessage)
+     * @return ISQLServerMessageHandler
+     */
+    ISQLServerMessageHandler setServerMessageHandler(ISQLServerMessageHandler messageHandler);
+
     /**
      * Returns the current flag for calcBigDecimalPrecision.
      *
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessage.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessage.java
new file mode 100644
index 00000000..622b865c
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessage.java
@@ -0,0 +1,99 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+package com.microsoft.sqlserver.jdbc;
+
+import java.sql.SQLException;
+
+
+/**
+ * Provides an interface SQLServerMessage
+ */
+public interface ISQLServerMessage {
+    /**
+     * Returns SQLServerError containing detailed info about SQL Server Message as received from SQL Server.
+     * 
+     * @return SQLServerError
+     */
+    public SQLServerError getSQLServerMessage();
+
+    /**
+     * Returns error message as received from SQL Server
+     * 
+     * @return Error Message
+     */
+    public String getErrorMessage();
+
+    /**
+     * Returns error number as received from SQL Server
+     * 
+     * @return Error Number
+     */
+    public int getErrorNumber();
+
+    /**
+     * Returns error state as received from SQL Server
+     * 
+     * @return Error State
+     */
+    public int getErrorState();
+
+    /**
+     * Returns Severity of error (as int value) as received from SQL Server
+     * 
+     * @return Error Severity
+     */
+    public int getErrorSeverity();
+
+    /**
+     * Returns name of the server where exception occurs as received from SQL Server
+     * 
+     * @return Server Name
+     */
+    public String getServerName();
+
+    /**
+     * Returns name of the stored procedure where exception occurs as received from SQL Server
+     * 
+     * @return Procedure Name
+     */
+    public String getProcedureName();
+
+    /**
+     * Returns line number where the error occurred in Stored Procedure returned by <code>getProcedureName()</code> as
+     * received from SQL Server
+     * 
+     * @return Line Number
+     */
+    public long getLineNumber();
+
+    /**
+     * Creates a SQLServerException or SQLServerWarning from this SQLServerMessage<br>
+     * 
+     * @return
+     *         <ul>
+     *         <li>SQLServerException if it's a SQLServerError object</li>
+     *         <li>SQLServerWarning if it's a SQLServerInfoMessage object</li>
+     *         </ul>
+     */
+    public SQLException toSqlExceptionOrSqlWarning();
+
+    /**
+     * Check if this is a isErrorMessage
+     * 
+     * @return true if it's an instance of SQLServerError
+     */
+    public default boolean isErrorMessage() {
+        return this instanceof SQLServerError;
+    }
+
+    /**
+     * Check if this is a SQLServerInfoMessage
+     * 
+     * @return true if it's an instance of SQLServerInfoMessage
+     */
+    public default boolean isInfoMessage() {
+        return this instanceof SQLServerInfoMessage;
+    }
+}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessageHandler.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessageHandler.java
new file mode 100644
index 00000000..2f0b71de
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessageHandler.java
@@ -0,0 +1,103 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+package com.microsoft.sqlserver.jdbc;
+
+/**
+ * You can use the ISQLServerMessageHandler interface to customize the way JDBC handles error messages generated by the SQL Server.
+ * Implementing ISQLServerMessageHandler in your own class for handling error messages can provide the following benefits:
+ * <ul>
+ * <li><b>"message feedback"</b><br>
+ * Display Server messages from a long running SQL Statement<br>
+ * Like <code>RAISERROR ('Progress message...', 0, 1) WITH NOWAIT</code><br>
+ * Or Status messages from a running backup...<br>
+ * </li>
+ * <li><b>"Universal" error logging</b><br>
+ * Your error-message handler can contain the logic for handling all error logging.
+ * </li>
+ * <li><b>"Universal" error handling</b><br>
+ * Error-handling logic can be placed in your error-message handler, instead of being repeated throughout your application.
+ * </li>
+ * <li><b>Remapping of error-message severity</b>, based on application requirements<br>
+ * Your error-message handler can contain logic for recognizing specific error messages, and downgrading or upgrading
+ * their severity based on application considerations rather than the severity rating of the server.
+ * For example, during a cleanup operation that deletes old rows, you might want to downgrade the severity of a
+ * message that a row does not exist. However, you may want to upgrade the severity in other circumstances.
+ * </li>
+ * </ul>
+ * <p>
+ * For example code, see {@link #messageHandler(ISQLServerMessage)}
+ */
+public interface ISQLServerMessageHandler {
+    /**
+     * You can use the ISQLServerMessageHandler interface to customize the way JDBC handles error messages generated by the SQL Server.
+     * Implementing ISQLServerMessageHandler in your own class for handling error messages can provide the following benefits:
+     * <ul>
+     * <li><b>"message feedback"</b><br>
+     * Display Server messages from a long running SQL Statement<br>
+     * Like <code>RAISERROR ('Progress message...', 0, 1) WITH NOWAIT</code><br>
+     * Or Status messages from a running backup...<br>
+     * </li>
+     * <li><b>"Universal" error logging</b><br>
+     * Your error-message handler can contain the logic for handling all error logging.
+     * </li>
+     * <li><b>"Universal" error handling</b><br>
+     * Error-handling logic can be placed in your error-message handler, instead of being repeated throughout your application.
+     * </li>
+     * <li><b>Remapping of error-message severity</b>, based on application requirements<br>
+     * Your error-message handler can contain logic for recognizing specific error messages, and downgrading or upgrading
+     * their severity based on application considerations rather than the severity rating of the server.
+     * For example, during a cleanup operation that deletes old rows, you might want to downgrade the severity of a
+     * message that a row does not exist. However, you may want to upgrade the severity in other circumstances.
+     * </li>
+     * </ul>
+     * 
+     * Example code:
+     * 
+     * <pre>
+     * public ISQLServerMessage messageHandler(ISQLServerMessage serverErrorOrWarning) {
+     *     ISQLServerMessage retObj = serverErrorOrWarning;
+     *
+     *     if (serverErrorOrWarning.isErrorMessage()) {
+     *
+     *         // Downgrade: 2601 -- Cannot insert duplicate key row...
+     *         if (2601 == serverErrorOrWarning.getErrorNumber()) {
+     *             retObj = serverErrorOrWarning.getSQLServerMessage().toSQLServerInfoMessage();
+     *         }
+     *
+     *         // Discard: 3701 -- Cannot drop the table ...
+     *         if (3701 == serverErrorOrWarning.getErrorNumber()) {
+     *             retObj = null;
+     *         }
+     *     }
+     *
+     *     return retObj;
+     * }
+     * 
+     * </pre>
+     * 
+     * @param serverErrorOrWarning
+     *        server error or warning
+     * @return
+     *         <ul>
+     *         <li><b>unchanged</b> same object as passed in.<br>
+     *         The JDBC driver will work as if no message hander was installed<br>
+     *         Possibly used for logging functionality<br>
+     *         </li>
+     *         <li><b>null</b><br>
+     *         The JDBC driver will <i>discard</i> this message. No SQLException will be thrown
+     *         </li>
+     *         <li><b>SQLServerInfoMessage</b> object<br>
+     *         Create a "SQL warning" from a input database error, and return it.
+     *         This results in the warning being added to the warning-message chain.
+     *         </li>
+     *         <li><b>SQLServerError</b> object<br>
+     *         If the originating message is a SQL warning (SQLServerInfoMessage object), messageHandler can evaluate
+     *         the SQL warning as urgent and create and return a SQL exception (SQLServerError object)
+     *         to be thrown once control is returned to the JDBC Driver.
+     *         </li>
+     *         </ul>
+     */
+    ISQLServerMessage messageHandler(ISQLServerMessage serverErrorOrWarning);
+}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/PersistentTokenCacheAccessAspect.java b/src/main/java/com/microsoft/sqlserver/jdbc/PersistentTokenCacheAccessAspect.java
index e388ac90..24458a7f 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/PersistentTokenCacheAccessAspect.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/PersistentTokenCacheAccessAspect.java
@@ -65,10 +65,21 @@ public class PersistentTokenCacheAccessAspect implements ITokenCacheAccessAspect
 
     }
 
+    /**
+     * Get expiry time
+     * 
+     * @return expiry time
+     */
     public long getExpiryTime() {
         return this.expiryTime;
     }
 
+    /**
+     * Set expiry time
+     * 
+     * @param expiryTime
+     *        expiry time
+     */
     public void setExpiryTime(long expiryTime) {
         this.expiryTime = expiryTime;
     }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java
index 195b197e..25f99214 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java
@@ -212,6 +212,47 @@ public class SQLServerBulkCSVFileRecord extends SQLServerBulkRecord implements j
         }
     }
 
+    /*
+     * RFC4180 specifies that rules for quoted fields. It allows quoted string data to contain newlines data
+     * provided the contents otherwise conforms to the rules for escaping quotes. For example, the following is valid:
+     * "a","b","c"
+     * "aaa","b <-- newline is retained in data field
+     * bb","c"
+     * "aa","bb","cc"
+     * We cannot simply use fileReader.readLine() to read these records but instead must continue reading until we reach
+     * a newline that is not contained within quotes.
+     */
+    private String readLineEscapeDelimiters() throws SQLServerException {
+        int quoteCount = 0;
+        StringBuilder sb = new StringBuilder();
+        try {
+            int c;
+            while ((c = fileReader.read()) != -1) {
+                if ((c == '\n' || c == '\r') && quoteCount % 2 == 0) { // newlines only end the record if we are not in quotes
+                    fileReader.mark(1);
+                    c = fileReader.read(); // we might have read \r of a \r\n, if so we need to read the \n as well
+                    if (c != '\n') {
+                        fileReader.reset(); // only delimited by \n, unread last char so it goes into the next record
+                    }
+                    break;
+                }
+                sb.append((char) c);
+                if (c == '"') {
+                    quoteCount++;
+                }
+            }
+            if (c == -1 && quoteCount % 2 != 0) { // stream ended, but we are within quotes -- data problem
+                throw new SQLServerException(SQLServerException.getErrString("R_InvalidCSVQuotes"), null, 0, null);
+            }
+            if (c == -1) { // keep semantics of readLine() by returning a null when there is no more data
+                return null;
+            }
+        } catch (IOException e) {
+            throw new SQLServerException(e.getMessage(), null, 0, e);
+        }
+        return sb.toString();
+    }
+
     private void initLoggerResources() {
         super.loggerPackageName = "com.microsoft.sqlserver.jdbc.SQLServerBulkCSVFileRecord";
     }
@@ -526,7 +567,7 @@ public class SQLServerBulkCSVFileRecord extends SQLServerBulkRecord implements j
     @Override
     public boolean next() throws SQLServerException {
         try {
-            currentLine = fileReader.readLine();
+            currentLine = escapeDelimiters ? readLineEscapeDelimiters() : fileReader.readLine();
         } catch (IOException e) {
             throw new SQLServerException(e.getMessage(), null, 0, e);
         }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java
index 89a433f0..42281688 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java
@@ -919,6 +919,22 @@ public class SQLServerColumnEncryptionAzureKeyVaultProvider extends SQLServerCol
         return isValid;
     }
 
+    /**
+     * Sign column master key metadata
+     * 
+     * @param masterKeyPath
+     *        master key path
+     * 
+     * @param allowEnclaveComputations
+     *        flag whether to allow enclave computations
+     * 
+     * @return
+     *         column master key metadata
+     * 
+     * @throws SQLServerException
+     *         when an error occurs
+     * 
+     */
     public byte[] signColumnMasterKeyMetadata(String masterKeyPath,
             boolean allowEnclaveComputations) throws SQLServerException {
         if (!allowEnclaveComputations) {
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java
index 50359f04..d2e2f28c 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java
@@ -164,6 +164,22 @@ public class SQLServerColumnEncryptionJavaKeyStoreProvider extends SQLServerColu
         return isValid;
     }
 
+    /**
+     * Sign column master key metadata
+     * 
+     * @param masterKeyPath
+     *        master key path
+     * 
+     * @param allowEnclaveComputations
+     *        flag whether to allow enclave computations
+     * 
+     * @return
+     *         column master key metadata
+     * 
+     * @throws SQLServerException
+     *         when an error occurs
+     * 
+     */
     public byte[] signColumnMasterKeyMetadata(String masterKeyPath,
             boolean allowEnclaveComputations) throws SQLServerException {
         if (!allowEnclaveComputations)
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
index b20b6e7f..00fbf59e 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
@@ -1053,6 +1053,9 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         this.ignoreOffsetOnDateTimeOffsetConversion = ignoreOffsetOnDateTimeOffsetConversion;
     }
 
+    /**
+     * Flag to indicate whether the driver should calculate precision for BigDecimal inputs, as opposed to using the maximum allowed valued for precision (38).
+     */
     private boolean calcBigDecimalPrecision = SQLServerDriverBooleanProperty.CALC_BIG_DECIMAL_PRECISION
             .getDefaultValue();
 
@@ -1794,6 +1797,11 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
     final void resetPooledConnection() {
         tdsChannel.resetPooledConnection();
         initResettableValues();
+
+        // reset prepared statement handle cache
+        if (null != preparedStatementHandleCache) {
+            preparedStatementHandleCache.clear();
+        }
     }
 
     /**
@@ -3207,9 +3215,15 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
 
             state = State.OPENED;
 
+            // Socket timeout is bounded by loginTimeout during the login phase.
+            // Reset socket timeout back to the original value.
+            tdsChannel.resetTcpSocketTimeout();
+
             if (connectionlogger.isLoggable(Level.FINER)) {
                 connectionlogger.finer(toString() + " End of connect");
             }
+        } catch (SocketException e) {
+            throw new SQLServerException(e.getMessage(), null);
         } finally {
             // once we exit the connect function, the connection can be only in one of two
             // states, Opened or Closed(if an exception occurred)
@@ -4782,6 +4796,8 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
 
         // Clean-up queue etc. related to batching of prepared statement discard actions (sp_unprepare).
         cleanupPreparedStatementDiscardActions();
+
+        ActivityCorrelator.cleanupActivityId();
     }
 
     /**
@@ -4803,6 +4819,8 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
             }
             notifyPooledConnection(null);
 
+            ActivityCorrelator.cleanupActivityId();
+
             if (connectionlogger.isLoggable(Level.FINER)) {
                 connectionlogger.finer(toString() + " Connection closed and returned to connection pool");
             }
@@ -4927,6 +4945,27 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         }
     }
 
+    // Any changes to SQLWarnings should be synchronized.
+    /** Used to add plain SQLWarning messages (if they do not hold extended information, like: ErrorSeverity, ServerName, ProcName etc */
+    void addWarning(SQLWarning sqlWarning) {
+        warningSynchronization.lock();
+        try {
+            if (null == sqlWarnings) {
+                sqlWarnings = sqlWarning;
+            } else {
+                sqlWarnings.setNextWarning(sqlWarning);
+            }
+        } finally {
+            warningSynchronization.unlock();
+        }
+    }
+
+    // Any changes to SQLWarnings should be synchronized.
+    /** Used to add messages that holds extended information, like: ErrorSeverity, ServerName, ProcName etc */
+    void addWarning(ISQLServerMessage sqlServerMessage) {
+        addWarning(new SQLServerWarning(sqlServerMessage.getSQLServerMessage()));
+    }
+
     @Override
     public void clearWarnings() throws SQLServerException {
         warningSynchronization.lock();
@@ -8499,6 +8538,31 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
     public String getIPAddressPreference() {
         return activeConnectionProperties.getProperty(SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString());
     }
+
+    /** Message handler */
+    private transient ISQLServerMessageHandler serverMessageHandler;
+
+    /**
+     * Set current message handler
+     * 
+     * @param messageHandler
+     *        message handler
+     * @return The previously installed message handler (null if none)
+     */
+    @Override
+    public ISQLServerMessageHandler setServerMessageHandler(ISQLServerMessageHandler messageHandler) {
+        ISQLServerMessageHandler installedMessageHandler = this.serverMessageHandler;
+        this.serverMessageHandler = messageHandler;
+        return installedMessageHandler;
+    }
+
+    /**
+     * @return Get Currently installed message handler on the connection
+     */
+    @Override
+    public ISQLServerMessageHandler getServerMessageHandler() {
+        return this.serverMessageHandler;
+    }
 }
 
 
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
index ffda2dd6..5ed4b7b0 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
@@ -692,6 +692,16 @@ class SQLServerConnectionPoolProxy implements ISQLServerConnection, java.io.Seri
         wrappedConnection.setAccessTokenCallbackClass(accessTokenCallbackClass);
     }
 
+    @Override
+    public ISQLServerMessageHandler getServerMessageHandler() {
+        return wrappedConnection.getServerMessageHandler();
+    }
+    
+    @Override
+    public ISQLServerMessageHandler setServerMessageHandler(ISQLServerMessageHandler messageHandler) {
+    	return wrappedConnection.setServerMessageHandler(messageHandler);
+    }
+
     /**
      * Returns the current value for 'calcBigDecimalPrecision'.
      *
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java
index 2dfb40d8..5c23a477 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java
@@ -268,7 +268,10 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
     // Use LinkedHashMap to force retrieve elements in order they were inserted
     /** getColumns columns */
     private LinkedHashMap<Integer, String> getColumnsDWColumns = null;
+
+    /** getTypes columns */
     private LinkedHashMap<Integer, String> getTypesDWColumns = null;
+
     /** getImportedKeys columns */
     private volatile LinkedHashMap<Integer, String> getImportedKeysDWColumns;
     private static final Lock LOCK = new ReentrantLock();
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerError.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerError.java
index 3454e5ae..b3e96904 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerError.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerError.java
@@ -6,12 +6,15 @@
 package com.microsoft.sqlserver.jdbc;
 
 import java.io.Serializable;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
 
 
 /**
  * SQLServerError represents a TDS error or message event.
  */
-public final class SQLServerError extends StreamPacket implements Serializable {
+public final class SQLServerError extends StreamPacket implements Serializable, ISQLServerMessage {
 
     /**
      * List SQL Server transient errors drivers will retry on from
@@ -221,6 +224,18 @@ public final class SQLServerError extends StreamPacket implements Serializable {
         super(TDS.TDS_ERR);
     }
 
+    SQLServerError(SQLServerError errorMsg) {
+        super(TDS.TDS_ERR);
+        this.errorNumber = errorMsg.errorNumber;
+        this.errorState = errorMsg.errorState;
+        this.errorSeverity = errorMsg.errorSeverity;
+        this.errorMessage = errorMsg.errorMessage;
+        this.serverName = errorMsg.serverName;
+        this.procName = errorMsg.procName;
+        this.lineNumber = errorMsg.lineNumber;
+    }
+
+    @Override
     void setFromTDS(TDSReader tdsReader) throws SQLServerException {
         if (TDS.TDS_ERR != tdsReader.readUnsignedByte())
             assert false;
@@ -237,4 +252,103 @@ public final class SQLServerError extends StreamPacket implements Serializable {
         procName = tdsReader.readUnicodeString(tdsReader.readUnsignedByte());
         lineNumber = tdsReader.readUnsignedInt();
     }
+
+    /**
+     * Holds any "overflow messages", or messages that has been added <b>after</b> the first message.
+     * <p>
+     * This is later on used when creating a SQLServerException.<br>
+     * Where all entries in the errorChain will be added {@link java.sql.SQLException#setNextException(SQLException)}
+     */
+    private List<SQLServerError> errorChain;
+
+    void addError(SQLServerError sqlServerError) {
+        if (errorChain == null) {
+            errorChain = new ArrayList<>();
+        }
+        errorChain.add(sqlServerError);
+    }
+
+    List<SQLServerError> getErrorChain() {
+        return errorChain;
+    }
+
+    @Override
+    public SQLServerError getSQLServerMessage() {
+        return this;
+    }
+
+    /**
+     * Downgrade a Error message into a Info message
+     * <p>
+     * This simply create a SQLServerInfoMessage from this SQLServerError,
+     * without changing the message content.
+     * 
+     * @return ISQLServerMessage
+     */
+    public ISQLServerMessage toSQLServerInfoMessage() {
+        return toSQLServerInfoMessage(-1, -1);
+    }
+
+    /**
+     * Downgrade a Error message into a Info message
+     * <p>
+     * This simply create a SQLServerInfoMessage from this SQLServerError,
+     * 
+     * @param newErrorSeverity
+     *        - The new ErrorSeverity
+     * 
+     * @return ISQLServerMessage
+     */
+    public ISQLServerMessage toSQLServerInfoMessage(int newErrorSeverity) {
+        return toSQLServerInfoMessage(newErrorSeverity, -1);
+    }
+
+    /**
+     * Downgrade a Error message into a Info message
+     * <p>
+     * This simply create a SQLServerInfoMessage from this SQLServerError,
+     * 
+     * @param newErrorSeverity
+     *        - If you want to change the ErrorSeverity (-1: leave unchanged)
+     * @param newErrorNumber
+     *        - If you want to change the ErrorNumber (-1: leave unchanged)
+     * 
+     * @return ISQLServerMessage
+     */
+    public ISQLServerMessage toSQLServerInfoMessage(int newErrorSeverity, int newErrorNumber) {
+        if (newErrorSeverity != -1) {
+            this.setErrorSeverity(newErrorSeverity);
+        }
+
+        if (newErrorNumber != -1) {
+            this.setErrorNumber(newErrorNumber);
+        }
+
+        return new SQLServerInfoMessage(this);
+    }
+
+    /**
+     * Set a new ErrorSeverity for this Message
+     * 
+     * @param newSeverity
+     *        new severity
+     */
+    public void setErrorSeverity(int newSeverity) {
+        this.errorSeverity = newSeverity;
+    }
+
+    /**
+     * Set a new ErrorNumber for this Message
+     * 
+     * @param newErrorNumber
+     *        new error number
+     */
+    public void setErrorNumber(int newErrorNumber) {
+        this.errorNumber = newErrorNumber;
+    }
+
+    @Override
+    public SQLException toSqlExceptionOrSqlWarning() {
+        return new SQLServerException(this);
+    }
 }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java
index 943804dc..77367708 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java
@@ -7,6 +7,7 @@ package com.microsoft.sqlserver.jdbc;
 
 import java.sql.SQLFeatureNotSupportedException;
 import java.text.MessageFormat;
+import java.util.List;
 import java.util.UUID;
 import java.util.logging.Level;
 
@@ -178,6 +179,15 @@ public final class SQLServerException extends java.sql.SQLException {
         logException(obj, errText, bStack);
     }
 
+    public SQLServerException(SQLServerError sqlServerError) {
+        super(sqlServerError.getErrorMessage(), 
+            generateStateCode(null, sqlServerError.getErrorNumber(), sqlServerError.getErrorState()), 
+            sqlServerError.getErrorNumber(), 
+            null);
+
+        this.sqlServerError = sqlServerError;
+	}
+
     /**
      * Constructs a new SQLServerException.
      * 
@@ -261,6 +271,21 @@ public final class SQLServerException extends java.sql.SQLException {
                 SQLServerException.checkAndAppendClientConnId(errText, con), state, sqlServerError, bStack);
         theException.setDriverErrorCode(DRIVER_ERROR_FROM_DATABASE);
 
+        // Add any extra messages to the SQLException error chain
+        List<SQLServerError> errorChain = sqlServerError.getErrorChain();
+        if (errorChain != null) {
+            for (SQLServerError srvError : errorChain) {
+                String state2 = generateStateCode(con, srvError.getErrorNumber(), srvError.getErrorState());
+
+                SQLServerException chainException = new SQLServerException(obj,
+                        SQLServerException.checkAndAppendClientConnId(srvError.getErrorMessage(), con), 
+                        state2, srvError, bStack);
+                chainException.setDriverErrorCode(DRIVER_ERROR_FROM_DATABASE);
+                
+                theException.setNextException(chainException);
+            }
+        }
+
         // Close the connection if we get a severity 20 or higher error class (nClass is severity of error).
         if ((sqlServerError.getErrorSeverity() >= 20) && (null != con)) {
             con.notifyPooledConnection(theException);
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerInfoMessage.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerInfoMessage.java
new file mode 100644
index 00000000..0b3a6075
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerInfoMessage.java
@@ -0,0 +1,135 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+
+package com.microsoft.sqlserver.jdbc;
+
+import java.sql.SQLException;
+
+
+/**
+ * Holds information about SQL Server messages that is considered as Informational Messages (normally if SQL Server Severity is at 10)
+ * <p>
+ * Instead of just holding the SQL Server message (like a normal SQLWarning, it also holds all the
+ * SQL Servers extended information, like: ErrorSeverity, ServerName, ProcName etc
+ * <p>
+ * This enables client to print out extra information about the message.<br>
+ * Like: In what procedure was the message produced.
+ * <p>
+ * A SQLServerInfoMessage is produced when reading the TDS Stream and added to the Connection as a SQLServerWarning
+ */
+public final class SQLServerInfoMessage extends StreamPacket implements ISQLServerMessage {
+    SQLServerError msg = new SQLServerError();
+
+    SQLServerInfoMessage() {
+        super(TDS.TDS_MSG);
+    }
+
+    SQLServerInfoMessage(SQLServerError msg) {
+        super(TDS.TDS_MSG);
+        this.msg = msg;
+    }
+
+    @Override
+    void setFromTDS(TDSReader tdsReader) throws SQLServerException {
+        if (TDS.TDS_MSG != tdsReader.readUnsignedByte())
+            assert false;
+        msg.setContentsFromTDS(tdsReader);
+    }
+
+    @Override
+    public SQLServerError getSQLServerMessage() {
+        return msg;
+    }
+
+    @Override
+    public String getErrorMessage() {
+        return msg.getErrorMessage();
+    }
+
+    @Override
+    public int getErrorNumber() {
+        return msg.getErrorNumber();
+    }
+
+    @Override
+    public int getErrorState() {
+        return msg.getErrorState();
+    }
+
+    @Override
+    public int getErrorSeverity() {
+        return msg.getErrorSeverity();
+    }
+
+    @Override
+    public String getServerName() {
+        return msg.getServerName();
+    }
+
+    @Override
+    public String getProcedureName() {
+        return msg.getProcedureName();
+    }
+
+    @Override
+    public long getLineNumber() {
+        return msg.getLineNumber();
+    }
+
+    /**
+     * Upgrade a Info message into a Error message
+     * <p>
+     * This simply create a SQLServerError from this SQLServerInfoMessage,
+     * without changing the message content.
+     * 
+     * @return ISQLServerMessage
+     */
+    public ISQLServerMessage toSQLServerError() {
+        return toSQLServerError(-1, -1);
+    }
+
+    /**
+     * Upgrade a Info message into a Error message
+     * <p>
+     * This simply create a SQLServerError from this SQLServerInfoMessage.
+     * 
+     * @param newErrorSeverity
+     *        - The new ErrorSeverity
+     * 
+     * @return ISQLServerMessage
+     */
+    public ISQLServerMessage toSQLServerError(int newErrorSeverity) {
+        return toSQLServerError(newErrorSeverity, -1);
+    }
+
+    /**
+     * Upgrade a Info message into a Error message
+     * <p>
+     * This simply create a SQLServerError from this SQLServerInfoMessage.
+     * 
+     * @param newErrorSeverity
+     *        - If you want to change the ErrorSeverity (-1: leave unchanged)
+     * @param newErrorNumber
+     *        - If you want to change the ErrorNumber (-1: leave unchanged)
+     * 
+     * @return ISQLServerMessage
+     */
+    public ISQLServerMessage toSQLServerError(int newErrorSeverity, int newErrorNumber) {
+        if (newErrorSeverity != -1) {
+            this.msg.setErrorSeverity(newErrorSeverity);
+        }
+
+        if (newErrorNumber != -1) {
+            this.msg.setErrorNumber(newErrorNumber);
+        }
+
+        return new SQLServerError(this.msg);
+    }
+
+    @Override
+    public SQLException toSqlExceptionOrSqlWarning() {
+        return new SQLServerWarning(this.msg);
+    }
+}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java
index 294a1aa8..4274e691 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java
@@ -71,10 +71,13 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements IS
     /** Processed SQL statement text, may not be same as what user initially passed. */
     final String userSQL;
 
+    // flag whether is exec escape syntax
     private boolean isExecEscapeSyntax;
 
+    // flag whether is call escape syntax
     private boolean isCallEscapeSyntax;
 
+    // flag whether is four part syntax
     private boolean isFourPartSyntax;
 
     /** Parameter positions in processed SQL statement text. */
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java
index 866b9ea1..cbfe102c 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java
@@ -1620,7 +1620,6 @@ public class SQLServerStatement implements ISQLServerStatement {
                         }
                     }
                 }
-
                 // If the current command (whatever it was) produced an error then stop parsing and propagate it up.
                 // In this case, the command is likely to be a RAISERROR, but it could be anything.
                 if (doneToken.isError())
@@ -1690,8 +1689,8 @@ public class SQLServerStatement implements ISQLServerStatement {
 
             @Override
             boolean onInfo(TDSReader tdsReader) throws SQLServerException {
-                StreamInfo infoToken = new StreamInfo();
-                infoToken.setFromTDS(tdsReader);
+                SQLServerInfoMessage infoMessage = new SQLServerInfoMessage();
+                infoMessage.setFromTDS(tdsReader);
 
                 // Under some circumstances the server cannot produce the cursored result set
                 // that we requested, but produces a client-side (default) result set instead.
@@ -1705,13 +1704,40 @@ public class SQLServerStatement implements ISQLServerStatement {
                 // ErrorCause: Server cursor is not supported on the specified SQL, falling back to default result set
                 // ErrorCorrectiveAction: None required
                 //
-                if (16954 == infoToken.msg.getErrorNumber())
+                if (16954 == infoMessage.msg.getErrorNumber())
                     executedSqlDirectly = true;
 
-                SQLWarning warning = new SQLWarning(
-                        infoToken.msg.getErrorMessage(), SQLServerException.generateStateCode(connection,
-                                infoToken.msg.getErrorNumber(), infoToken.msg.getErrorState()),
-                        infoToken.msg.getErrorNumber());
+
+                // Call the message handler to see what that think of the message
+                // - discard
+                // - upgrade to Error
+                // - or simply pass on
+                ISQLServerMessageHandler msgHandler = ((ISQLServerConnection)getConnection()).getServerMessageHandler();
+                if (msgHandler != null) {
+
+                    // Let the message handler decide if the error should be unchanged, up/down-graded or ignored
+                    ISQLServerMessage srvMessage = msgHandler.messageHandler(infoMessage);
+                
+                    // Ignored
+                    if (srvMessage == null) {
+                    	return true;
+                    }
+                
+                    // The message handler changed it to an "Error Message"
+                    if (srvMessage.isErrorMessage()) {
+                        // Set/Add the error message to the "super"
+                        addDatabaseError( (SQLServerError)srvMessage );
+                        return true;
+                    }
+                    
+                    // Still a "info message", just set infoMessage and the code in the below section will create the Warnings
+                    if (srvMessage.isInfoMessage()) {
+                        infoMessage = (SQLServerInfoMessage)srvMessage;
+                    }
+                }
+
+                // Create the SQLWarning and add them to the Warning chain
+                SQLWarning warning = new SQLServerWarning(infoMessage.msg);
 
                 if (sqlWarnings == null) {
                     sqlWarnings = new Vector<>();
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerWarning.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerWarning.java
new file mode 100644
index 00000000..81d588bd
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerWarning.java
@@ -0,0 +1,47 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+package com.microsoft.sqlserver.jdbc;
+
+import java.sql.SQLWarning;
+
+/**
+ * Holds information about SQL Server messages that is considered as Informational Messages (normally if SQL Server Severity is at 10)
+ * <p>
+ * Instead of just holding the SQL Server message (like a normal SQLWarning, it also holds all the 
+ * SQL Servers extended information, like: ErrorSeverity, ServerName, ProcName etc
+ * <p>
+ * This enables client to print out extra information about the message.<br>
+ * Like: In what procedure was the message produced.
+ */
+public class SQLServerWarning
+extends SQLWarning
+{
+    private static final long serialVersionUID = -5212432397705929142L;
+
+    /** SQL server error */
+    private SQLServerError sqlServerError;
+    
+    /*
+     * Create a SQLWarning from an SQLServerError object
+     */
+    public SQLServerWarning(SQLServerError sqlServerError) {
+        super(sqlServerError.getErrorMessage(), 
+            SQLServerException.generateStateCode(null, sqlServerError.getErrorNumber(), sqlServerError.getErrorState()), 
+            sqlServerError.getErrorNumber(), 
+            null);
+
+        this.sqlServerError = sqlServerError;
+    }
+
+    /**
+     * Returns SQLServerError object containing detailed info about exception as received from SQL Server. This API
+     * returns null if no server error has occurred.
+     * 
+     * @return SQLServerError
+     */
+    public SQLServerError getSQLServerError() {
+        return sqlServerError;
+    }
+}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/StreamInfo.java b/src/main/java/com/microsoft/sqlserver/jdbc/StreamInfo.java
deleted file mode 100644
index 5b4cb571..00000000
--- a/src/main/java/com/microsoft/sqlserver/jdbc/StreamInfo.java
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
- * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
- */
-
-package com.microsoft.sqlserver.jdbc;
-
-final class StreamInfo extends StreamPacket {
-    final SQLServerError msg = new SQLServerError();
-
-    StreamInfo() {
-        super(TDS.TDS_MSG);
-    }
-
-    void setFromTDS(TDSReader tdsReader) throws SQLServerException {
-        if (TDS.TDS_MSG != tdsReader.readUnsignedByte())
-            assert false;
-        msg.setContentsFromTDS(tdsReader);
-    }
-}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java
index 32a1c762..3c08a80c 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java
@@ -1551,7 +1551,10 @@ final class DTV {
                     break;
 
                 case GUID:
-                    op.execute(this, (UUID) null);
+                    if (null != cryptoMeta)
+                        op.execute(this, (byte[]) null);
+                    else
+                        op.execute(this, (UUID) null);
                     break;
 
                 case TINYINT:
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java b/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java
index c18d61a0..90eebb8d 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java
@@ -197,6 +197,14 @@ class TDSTokenHandler {
         return databaseError;
     }
 
+    public void addDatabaseError(SQLServerError databaseError) {
+        if (this.databaseError == null) {
+            this.databaseError = databaseError;
+        } else {
+            this.databaseError.addError(databaseError);
+        }
+    }
+
     TDSTokenHandler(String logContext) {
         this.logContext = logContext;
     }
@@ -258,13 +266,29 @@ class TDSTokenHandler {
     }
 
     boolean onError(TDSReader tdsReader) throws SQLServerException {
-        if (null == databaseError) {
-            databaseError = new SQLServerError();
-            databaseError.setFromTDS(tdsReader);
-        } else {
-            (new SQLServerError()).setFromTDS(tdsReader);
+        SQLServerError tmpDatabaseError = new SQLServerError();
+        tmpDatabaseError.setFromTDS(tdsReader);
+
+        ISQLServerMessageHandler msgHandler = tdsReader.getConnection().getServerMessageHandler();
+        if (msgHandler != null) {
+            // Let the message handler decide if the error should be unchanged/down-graded or ignored
+            ISQLServerMessage srvMessage = msgHandler.messageHandler(tmpDatabaseError);
+
+            // Ignored
+            if (srvMessage == null) {
+                return true;
+            }
+
+            // Down-graded to a SQLWarning
+            if (srvMessage.isInfoMessage()) {
+                tdsReader.getConnection().addWarning(srvMessage);
+                return true;
+            }
         }
 
+        // set/add the database error
+        addDatabaseError(tmpDatabaseError);
+
         return true;
     }
 


Test patch:
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyCSVTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyCSVTest.java
index 48e28b71..97b0e3f9 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyCSVTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyCSVTest.java
@@ -153,7 +153,7 @@ public class BulkCopyCSVTest extends AbstractTest {
         /*
          * The list below is the copy of inputFileDelimiterEsc ape with quotes removed.
          */
-        String[][] expectedEscaped = new String[11][4];
+        String[][] expectedEscaped = new String[12][4];
         expectedEscaped[0] = new String[] {"test", " test\"", "no@split", " testNoQuote", ""};
         expectedEscaped[1] = new String[] {null, null, null, null, ""};
         expectedEscaped[2] = new String[] {"\"", "test\"test", "test@\"  test", null, ""};
@@ -166,6 +166,7 @@ public class BulkCopyCSVTest extends AbstractTest {
         expectedEscaped[8] = new String[] {"1997", "Ford", "E350", "Super@ \"luxurious\" truck", ""};
         expectedEscaped[9] = new String[] {"1997", "Ford", "E350", "E63", ""};
         expectedEscaped[10] = new String[] {"1997", "Ford", "E350", " Super luxurious truck ", ""};
+        expectedEscaped[11] = new String[] {"1997", "F\r\no\r\nr\r\nd", "E350", "\"Super\" \"luxurious\" \"truck\"", ""};
 
         try (Connection con = getConnection(); Statement stmt = con.createStatement();
                 SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con);
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/PoolingTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/PoolingTest.java
index a23d6151..d99d846e 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/PoolingTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/PoolingTest.java
@@ -23,11 +23,10 @@ import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
-import org.junit.platform.runner.JUnitPlatform;
-import org.junit.runner.RunWith;
 
 import com.microsoft.sqlserver.jdbc.ISQLServerConnection;
 import com.microsoft.sqlserver.jdbc.RandomUtil;
+import com.microsoft.sqlserver.jdbc.SQLServerConnectionPoolDataSource;
 import com.microsoft.sqlserver.jdbc.SQLServerXADataSource;
 import com.microsoft.sqlserver.jdbc.TestResource;
 import com.microsoft.sqlserver.jdbc.TestUtils;
@@ -42,7 +41,6 @@ import com.zaxxer.hikari.HikariDataSource;
  * Tests pooled connection
  *
  */
-@RunWith(JUnitPlatform.class)
 public class PoolingTest extends AbstractTest {
     static String tempTableName = RandomUtil.getIdentifier("#poolingtest");
     static String tableName = RandomUtil.getIdentifier("PoolingTestTable");
@@ -217,6 +215,69 @@ public class PoolingTest extends AbstractTest {
         }
     }
 
+    /**
+     * test that prepared statement cache is cleared when disableStatementPooling is not set
+     */
+    @Test
+    public void testDisableStatementPooling() throws SQLException {
+        SQLServerConnectionPoolDataSource ds = new SQLServerConnectionPoolDataSource();
+        ds.setURL(connectionString + ";disableStatementPooling=false;statementPoolingCacheSize=20");
+        PooledConnection pConn = ds.getPooledConnection();
+
+        // create test table
+        try (Connection conn = pConn.getConnection(); Statement stmt = conn.createStatement()) {
+            TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt);
+
+            stmt.execute(
+                    "create table " + AbstractSQLGenerator.escapeIdentifier(tableName) + " (c1 int, c2 varchar(20))");
+        }
+
+        try {
+            for (int i = 0; i < 5; i++) {
+                try (Connection conn = pConn.getConnection();) {
+                    conn.setAutoCommit(false);
+
+                    try (Statement stmt = conn.createStatement(); PreparedStatement pstmt = conn.prepareStatement(
+                            "insert into " + AbstractSQLGenerator.escapeIdentifier(tableName) + " values (?, ?)")) {
+
+                        for (int j = 0; j < 3; j++) {
+                            pstmt.setInt(1, j);
+                            pstmt.setString(2, "test" + j);
+                            pstmt.addBatch();
+                        }
+
+                        pstmt.executeBatch();
+                        conn.commit();
+                    }
+                }
+            }
+
+            try (Connection conn = pConn.getConnection(); Statement stmt = conn.createStatement();
+                    ResultSet rs = stmt.executeQuery("select * from " + AbstractSQLGenerator.escapeIdentifier(tableName)
+                            + " order by c1 desc")) {
+
+                int i = 1;
+                while (rs.next()) {
+                    if (i <= 5) {
+                        assertEquals(2, rs.getInt(1));
+                        assertEquals("test2", rs.getString(2));
+                    } else if (i > 5 && i <= 10) {
+                        assertEquals(1, rs.getInt(1));
+                        assertEquals("test1", rs.getString(2));
+                    } else if (i > 10 && i <= 15) {
+                        assertEquals(0, rs.getInt(1));
+                        assertEquals("test0", rs.getString(2));
+                    }
+                    i++;
+                }
+            }
+        } finally {
+            if (null != pConn) {
+                pConn.close();
+            }
+        }
+    }
+
     /**
      * setup connection, get connection from pool, and test threads
      * 
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java
index d758950f..926b6a76 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java
@@ -521,6 +521,10 @@ public class RequestBoundaryMethodsTest extends AbstractTest {
         verifiedMethodNames.add("setMsiTokenCacheTtl");
         verifiedMethodNames.add("getAccessTokenCallbackClass");
         verifiedMethodNames.add("setAccessTokenCallbackClass");
+        verifiedMethodNames.add("getServerMessageHandler");
+        verifiedMethodNames.add("setServerMessageHandler");
+        verifiedMethodNames.add("getCalcBigDecimalScale");
+        verifiedMethodNames.add("setCalcBigDecimalScale");
         verifiedMethodNames.add("getUseFlexibleCallableStatements");
         verifiedMethodNames.add("setUseFlexibleCallableStatements");
         verifiedMethodNames.add("getCalcBigDecimalPrecision");
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 68fbfc8d..268fda73 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java
@@ -313,6 +313,19 @@ public class TimeoutTest extends AbstractTest {
                 "total time: " + totalTime + " interval: " + TimeUnit.SECONDS.toMillis(interval));
     }
 
+    @Test
+    @Tag(Constants.xAzureSQLDW)
+    public void testSocketTimeoutBoundedByLoginTimeoutReset() throws Exception {
+        try (Connection con = PrepUtil.getConnection(connectionString + ";socketTimeout=90000;loginTimeout=10;");
+                Statement stmt = con.createStatement()) {
+            // Login timeout (10s) is less than the 15s sec WAITFOR DELAY. Upon a login attempt, socketTimeout should be bounded
+            // by loginTimeout. After a successful login, when executing a query, socketTimeout should be reset to the
+            // original 90000ms timeout. The statement below should successfully execute as socketTimeout should not be bounded
+            // by loginTimeout, otherwise the test fails with a socket read timeout error.
+            stmt.execute("WAITFOR DELAY '00:00:15';");
+        }
+    }
+
     // Test for detecting Azure server for connection retries
     @Test
     public void testAzureEndpointRetry() {
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/GuidTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/GuidTest.java
index e8b5c157..2e0968eb 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/GuidTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/GuidTest.java
@@ -1,97 +1,99 @@
-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);
-            }
-        }
-    }
-
-}
+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.Tag;
+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;
+
+import com.microsoft.sqlserver.testframework.Constants;
+/*
+ * 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
+    @Tag(Constants.xAzureSQLDW)
+    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/exception/ChainedExceptionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/exception/ChainedExceptionTest.java
new file mode 100644
index 00000000..4c05d2fb
--- /dev/null
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/exception/ChainedExceptionTest.java
@@ -0,0 +1,74 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+package com.microsoft.sqlserver.jdbc.exception;
+
+import static org.junit.Assert.fail;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.runner.JUnitPlatform;
+import org.junit.runner.RunWith;
+
+import com.microsoft.sqlserver.jdbc.TestResource;
+import com.microsoft.sqlserver.testframework.AbstractTest;
+import com.microsoft.sqlserver.testframework.Constants;
+
+
+@RunWith(JUnitPlatform.class)
+public class ChainedExceptionTest extends AbstractTest {
+
+    @BeforeAll
+    public static void setupTests() throws Exception {
+        setConnection();
+    }
+
+    @Test
+    @Tag(Constants.xAzureSQLDW)
+    public void testTwoExceptions() throws Exception {
+        try (Connection conn = getConnection()) {
+
+            // The below should yield the following Server Messages:
+            // 1 : Msg 5074, Level 16, State 1: The object 'DF__#chained_exc__c1__AB25243A' is dependent on column 'c1'.
+            // 1 : Msg 4922, Level 16, State 9: ALTER TABLE ALTER COLUMN c1 failed because one or more objects access this column.
+            try (Statement stmnt = conn.createStatement()) {
+                stmnt.executeUpdate("CREATE TABLE #chained_exception_test_x1(c1 INT DEFAULT(0))");
+                stmnt.executeUpdate("ALTER  TABLE #chained_exception_test_x1 ALTER COLUMN c1 VARCHAR(10)");
+                stmnt.executeUpdate("DROP   TABLE IF EXISTS #chained_exception_test_x1");
+            }
+
+            fail(TestResource.getResource("R_expectedFailPassed"));
+
+        } catch (SQLException ex) {
+
+            // Check the SQLException and the chain
+            int exCount = 0;
+            int firstMsgNum = ex.getErrorCode();
+            int lastMsgNum = -1;
+
+            while (ex != null) {
+                exCount++;
+
+                lastMsgNum = ex.getErrorCode();
+
+                ex = ex.getNextException();
+            }
+
+            // Exception Count should be: 2
+            assertEquals(2, exCount, "Number of SQLExceptions in the SQLException chain");
+
+            // Check first Msg: 5074
+            assertEquals(5074, firstMsgNum, "First SQL Server Message");
+
+            // Check last Msg: 4922
+            assertEquals(4922, lastMsgNum, "Last SQL Server Message");
+        }
+    }
+}
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/messageHandler/MessageHandlerTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/messageHandler/MessageHandlerTest.java
new file mode 100644
index 00000000..b9d52a68
--- /dev/null
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/messageHandler/MessageHandlerTest.java
@@ -0,0 +1,474 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+package com.microsoft.sqlserver.jdbc.messageHandler;
+
+import static org.junit.Assert.fail;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import org.junit.jupiter.api.Tag;
+
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.SQLWarning;
+import java.sql.Statement;
+import java.sql.Types;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.runner.JUnitPlatform;
+import org.junit.runner.RunWith;
+
+import com.microsoft.sqlserver.jdbc.ISQLServerMessage;
+import com.microsoft.sqlserver.jdbc.ISQLServerMessageHandler;
+import com.microsoft.sqlserver.jdbc.SQLServerConnection;
+import com.microsoft.sqlserver.jdbc.TestResource;
+import com.microsoft.sqlserver.testframework.AbstractTest;
+import com.microsoft.sqlserver.testframework.Constants;
+
+
+@RunWith(JUnitPlatform.class)
+public class MessageHandlerTest extends AbstractTest {
+
+    @BeforeAll
+    public static void setupTests() throws Exception {
+        setConnection();
+    }
+
+    /**
+     * Helper method to count number of SQLWarnings in a chain
+     * 
+     * @param str
+     *        - Debug String, so we can evaluate from where we called it...
+     * @param sqlw
+     *        - The SQL Warning chain. (can be null)
+     * @return A count of warnings
+     */
+    private static int getWarningCount(String str, SQLWarning sqlw) {
+        int count = 0;
+        while (sqlw != null) {
+            count++;
+            // System.out.println("DEBUG: getWarningCount(): [" + str + "] SQLWarning: Error=" + sqlw.getErrorCode() + ", Severity=" + ((SQLServerWarning)sqlw).getSQLServerError().getErrorSeverity() + ", Text=|" + sqlw.getMessage() + "|.");
+            sqlw = sqlw.getNextWarning();
+        }
+        return count;
+    }
+
+    /**
+     * Test message handler with normal Statement
+     * <ul>
+     * <li>Insert duplicate row -- Mapped to Info Message</li>
+     * <li>Drop table that do not exist -- Mapped to ignore</li>
+     * </ul>
+     */
+    @Test
+    @Tag(Constants.xAzureSQLDW)
+    public void testMsgHandlerWithStatement() throws Exception {
+        try (SQLServerConnection conn = getConnection()) {
+
+            class TestMsgHandler implements ISQLServerMessageHandler {
+                int numOfCalls = 0;
+                int numOfDowngrades = 0;
+                int numOfDiscards = 0;
+
+                @Override
+                public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) {
+                    numOfCalls++;
+                    ISQLServerMessage retObj = srvErrorOrWarning;
+
+                    if (srvErrorOrWarning.isErrorMessage()) {
+
+                        // Downgrade: 2601 -- Cannot insert duplicate key row in object 'dbo.#msghandler_tmp_table' with unique index 'ix_id'. The duplicate key value is (1)
+                        if (2601 == srvErrorOrWarning.getErrorNumber()) {
+                            retObj = srvErrorOrWarning.getSQLServerMessage().toSQLServerInfoMessage();
+                            numOfDowngrades++;
+                        }
+
+                        // Discard: 3701 -- Cannot drop the table '#msghandler_tmp_table', because it does not exist or you do not have permission.
+                        if (3701 == srvErrorOrWarning.getErrorNumber()) {
+                            retObj = null;
+                            numOfDiscards++;
+                        }
+                    }
+
+                    if (srvErrorOrWarning.isInfoMessage()) {
+
+                        // Discard: 3621 -- The statement has been terminated.
+                        if (3621 == srvErrorOrWarning.getErrorNumber()) {
+                            retObj = null;
+                            numOfDiscards++;
+                        }
+                    }
+
+                    return retObj;
+                }
+            }
+            TestMsgHandler testMsgHandler = new TestMsgHandler();
+
+            // Create a massage handler
+            conn.setServerMessageHandler(testMsgHandler);
+
+            try (Statement stmnt = conn.createStatement()) {
+
+                stmnt.executeUpdate("CREATE TABLE #msghandler_tmp_table(id int, c1 varchar(255))");
+                assertNull(conn.getWarnings(), "Expecting NO SQLWarnings from 'create table', at Connection.");
+                assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'create table', at Statement.");
+
+                stmnt.executeUpdate("CREATE UNIQUE INDEX ix_id ON #msghandler_tmp_table(id)");
+                assertNull(conn.getWarnings(), "Expecting NO SQLWarnings from 'create index', at Connection.");
+                assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'create index', at Statement.");
+
+                stmnt.executeUpdate("INSERT INTO #msghandler_tmp_table VALUES(1, 'row 1')");
+                assertNull(conn.getWarnings(), "Expecting NO SQLWarnings from 'first insert', at Connection.");
+                assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'first insert', at Statement.");
+
+                stmnt.executeUpdate(
+                        "INSERT INTO #msghandler_tmp_table VALUES(1, 'row 1 - again - msg handler downgrades it')");
+                assertNotNull(conn.getWarnings(),
+                        "Expecting at least ONE SQLWarnings from 'second insert', which is a duplicate row, at Connection.");
+                assertNull(stmnt.getWarnings(),
+                        "Expecting NO SQLWarnings from 'second insert', which is a duplicate row, at Statement.");
+                conn.clearWarnings(); // Clear Warnings at Connection level
+
+                stmnt.executeUpdate("DROP TABLE #msghandler_tmp_table");
+                assertNull(conn.getWarnings(), "Expecting NO SQLWarnings from 'first drop table', at Connection.");
+                assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'first drop table', at Statement.");
+
+                stmnt.executeUpdate("DROP TABLE #msghandler_tmp_table"); // This should be IGNORED by the message handler
+                assertNull(conn.getWarnings(),
+                        "Expecting NO SQLWarnings from 'second drop table, since it should be IGNORED by the message handler', at Connection.");
+                assertNull(stmnt.getWarnings(),
+                        "Expecting NO SQLWarnings from 'second drop table, since it should be IGNORED by the message handler', at Statement.");
+
+                // numOfCalls to the message handler should be: 3
+                assertEquals(3, testMsgHandler.numOfCalls, "Number of message calls to the message handler.");
+
+                // numOfDowngrades in the message handler should be: 1
+                assertEquals(1, testMsgHandler.numOfDowngrades, "Number of message Downgrades in the message handler.");
+
+                // numOfDiscards in the message handler should be: 2
+                assertEquals(2, testMsgHandler.numOfDiscards, "Number of message Discards in the message handler.");
+            }
+
+        } catch (SQLException ex) {
+            fail(TestResource.getResource("R_unexpectedErrorMessage") + ex.getMessage());
+        }
+    }
+
+    /**
+     * Test message handler with PreparedStatement
+     * <ul>
+     * <li>Insert duplicate row -- Mapped to Info Message</li>
+     * <li>Drop table that do not exist -- Mapped to ignore</li>
+     * </ul>
+     */
+    @Test
+    @Tag(Constants.xAzureSQLDW)
+    public void testMsgHandlerWithPreparedStatement() throws Exception {
+        try (SQLServerConnection conn = getConnection()) {
+
+            class TestMsgHandler implements ISQLServerMessageHandler {
+                int numOfCalls = 0;
+                int numOfDowngrades = 0;
+                int numOfDiscards = 0;
+
+                @Override
+                public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) {
+                    numOfCalls++;
+                    ISQLServerMessage retObj = srvErrorOrWarning;
+
+                    if (srvErrorOrWarning.isErrorMessage()) {
+
+                        // Downgrade: 2601 -- Cannot insert duplicate key row in object 'dbo.#msghandler_tmp_table' with unique index 'ix_id'. The duplicate key value is (1)
+                        if (2601 == srvErrorOrWarning.getErrorNumber()) {
+                            retObj = srvErrorOrWarning.getSQLServerMessage().toSQLServerInfoMessage();
+                            numOfDowngrades++;
+                        }
+
+                        // Discard: 3701 -- Cannot drop the table '#msghandler_tmp_table', because it does not exist or you do not have permission.
+                        if (3701 == srvErrorOrWarning.getErrorNumber()) {
+                            retObj = null;
+                            numOfDiscards++;
+                        }
+                    }
+
+                    if (srvErrorOrWarning.isInfoMessage()) {
+
+                        // Discard: 3621 -- The statement has been terminated.
+                        if (3621 == srvErrorOrWarning.getErrorNumber()) {
+                            retObj = null;
+                            numOfDiscards++;
+                        }
+                    }
+
+                    return retObj;
+                }
+            }
+            TestMsgHandler testMsgHandler = new TestMsgHandler();
+
+            // Create a massage handler
+            conn.setServerMessageHandler(testMsgHandler);
+
+            try (Statement stmnt = conn.createStatement()) {
+                stmnt.executeUpdate("CREATE TABLE #msghandler_tmp_table(id int, c1 varchar(255))");
+                assertNull(conn.getWarnings(), "Expecting NO SQLWarnings from 'create table', at Connection.");
+                assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'create table', at Statement.");
+            }
+
+            try (Statement stmnt = conn.createStatement()) {
+                stmnt.executeUpdate("CREATE UNIQUE INDEX ix_id ON #msghandler_tmp_table(id)");
+                assertNull(conn.getWarnings(), "Expecting NO SQLWarnings from 'create index', at Connection.");
+                assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'create index', at Statement.");
+            }
+
+            try (PreparedStatement stmnt = conn.prepareStatement("INSERT INTO #msghandler_tmp_table VALUES(?, ?)")) {
+                stmnt.setInt(1, 1);
+                stmnt.setString(2, "row 1");
+                stmnt.executeUpdate();
+                assertNull(conn.getWarnings(), "Expecting NO SQLWarnings from 'first insert', at Connection.");
+                assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'first insert', at Statement.");
+            }
+
+            try (PreparedStatement stmnt = conn.prepareStatement("INSERT INTO #msghandler_tmp_table VALUES(?, ?)")) {
+                stmnt.setInt(1, 1);
+                stmnt.setString(2, "row 1 - again - msg handler downgrades it");
+                stmnt.executeUpdate();
+                assertNotNull(conn.getWarnings(),
+                        "Expecting at least ONE SQLWarnings from 'second insert', which is a duplicate row, at Connection.");
+                assertNull(stmnt.getWarnings(),
+                        "Expecting NO SQLWarnings from 'second insert', which is a duplicate row, at Statement.");
+                conn.clearWarnings(); // Clear Warnings at Connection level
+            }
+
+            try (Statement stmnt = conn.createStatement()) {
+                stmnt.executeUpdate("DROP TABLE #msghandler_tmp_table");
+                assertNull(conn.getWarnings(), "Expecting NO SQLWarnings from 'first drop table', at Connection.");
+                assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'first drop table', at Statement.");
+            }
+
+            try (Statement stmnt = conn.createStatement()) {
+                stmnt.executeUpdate("DROP TABLE #msghandler_tmp_table");
+                assertNull(conn.getWarnings(), "Expecting NO SQLWarnings from 'first drop table', at Connection.");
+                assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'first drop table', at Statement.");
+            }
+
+            // numOfCalls to the message handler should be: 3
+            assertEquals(3, testMsgHandler.numOfCalls, "Number of message calls to the message handler.");
+
+            // numOfDowngrades in the message handler should be: 1
+            assertEquals(1, testMsgHandler.numOfDowngrades, "Number of message Downgrades in the message handler.");
+
+            // numOfDiscards in the message handler should be: 2
+            assertEquals(2, testMsgHandler.numOfDiscards, "Number of message Discards in the message handler.");
+
+        } catch (SQLException ex) {
+            fail(TestResource.getResource("R_unexpectedErrorMessage") + ex.getMessage());
+        }
+    }
+
+    /**
+     * Test message handler with CallableStatement
+     * <ul>
+     * <li>Insert duplicate row -- Mapped to Info Message</li>
+     * <li>Drop table that do not exist -- Mapped to ignore</li>
+     * </ul>
+     */
+    @Test
+    @Tag(Constants.xAzureSQLDW)
+    public void testMsgHandlerWithCallableStatement() throws Exception {
+        try (SQLServerConnection conn = getConnection()) {
+
+            class TestMsgHandler implements ISQLServerMessageHandler {
+                int numOfCalls = 0;
+                int numOfDowngrades = 0;
+                int numOfDiscards = 0;
+
+                @Override
+                public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) {
+                    numOfCalls++;
+                    ISQLServerMessage retObj = srvErrorOrWarning;
+
+                    if (srvErrorOrWarning.isErrorMessage()) {
+
+                        // Downgrade: 2601 -- Cannot insert duplicate key row in object 'dbo.#msghandler_tmp_table' with unique index 'ix_id'. The duplicate key value is (1)
+                        if (2601 == srvErrorOrWarning.getErrorNumber()) {
+                            retObj = srvErrorOrWarning.getSQLServerMessage().toSQLServerInfoMessage();
+                            numOfDowngrades++;
+                        }
+
+                        // Downgrade: 3701 -- Cannot drop the table '#msghandler_tmp_table', because it does not exist or you do not have permission.
+                        if (3701 == srvErrorOrWarning.getErrorNumber()) {
+                            retObj = null;
+                            numOfDiscards++;
+                        }
+                    }
+
+                    if (srvErrorOrWarning.isInfoMessage()) {
+
+                        // Discard: 3621 -- The statement has been terminated.
+                        if (3621 == srvErrorOrWarning.getErrorNumber()) {
+                            retObj = null;
+                            numOfDiscards++;
+                        }
+                    }
+
+                    return retObj;
+                }
+            }
+            TestMsgHandler testMsgHandler = new TestMsgHandler();
+
+            // Create a massage handler
+            conn.setServerMessageHandler(testMsgHandler);
+
+            // SQL to create procedure
+            String sqlCreateProc = "" + "CREATE PROCEDURE #msghandler_tmp_proc( \n" + "    @out_row_count INT OUTPUT \n"
+                    + ") \n" + "AS \n" + "BEGIN \n" + "    -- Create a dummy table, with index \n"
+                    + "    CREATE TABLE #msghandler_tmp_table(id int, c1 varchar(255)) \n"
+                    + "    CREATE UNIQUE INDEX ix_id ON #msghandler_tmp_table(id) \n" + "     \n"
+                    + "    -- Insert records 1 \n" + "    INSERT INTO #msghandler_tmp_table VALUES(1, 'row 1') \n"
+                    + "     \n"
+                    + "    -- Insert records 1 -- Again, which will FAIL, but the message handler will downgrade it into a INFO Message \n"
+                    + "    INSERT INTO #msghandler_tmp_table VALUES(1, 'row 1 - again - msg handler downgrades it') \n"
+                    + "     \n" + "    -- Count records \n" + "    SELECT @out_row_count = count(*) \n"
+                    + "    FROM #msghandler_tmp_table \n" + "     \n" + "    -- Drop the table \n"
+                    + "    DROP TABLE #msghandler_tmp_table \n" + "     \n"
+                    + "    -- Drop the table agin... The message handler will DISCARD the error\n"
+                    + "    DROP TABLE #msghandler_tmp_table \n" + "     \n" + "    RETURN 1 \n" + "END \n";
+
+            // Create the proc
+            try (Statement stmnt = conn.createStatement()) {
+                stmnt.executeUpdate(sqlCreateProc);
+                assertEquals(0, getWarningCount("Conn", conn.getWarnings()),
+                        "Expecting NO SQLWarnings from 'create proc', at Connection.");
+                assertEquals(0, getWarningCount("Stmnt", conn.getWarnings()),
+                        "Expecting NO SQLWarnings from 'create proc', at Statement.");
+            }
+
+            // Execute the proc
+            try (CallableStatement cstmnt = conn.prepareCall("{ ? =call  #msghandler_tmp_proc(?) }")) {
+                cstmnt.registerOutParameter(1, Types.INTEGER);
+                cstmnt.registerOutParameter(2, Types.INTEGER);
+                cstmnt.execute();
+                int procReturnCode = cstmnt.getInt(1);
+                int procRowCount = cstmnt.getInt(2);
+
+                assertEquals(1, procReturnCode, "Expecting ReturnCode 1 from the temp procedure.");
+                assertEquals(1, procRowCount, "Expecting procRowCount 1 from the temp procedure.");
+
+                assertEquals(1, getWarningCount("Conn", conn.getWarnings()),
+                        "Expecting NO SQLWarnings from 'exec proc', at Connection.");
+                assertEquals(0, getWarningCount("CStmnt", cstmnt.getWarnings()),
+                        "Expecting NO SQLWarnings from 'exec proc', at CallableStatement.");
+            }
+
+            // numOfCalls to the message handler should be: 3
+            assertEquals(3, testMsgHandler.numOfCalls, "Number of message calls to the message handler.");
+
+            // numOfDowngrades in the message handler should be: 1
+            assertEquals(1, testMsgHandler.numOfDowngrades, "Number of message Downgrades in the message handler.");
+
+            // numOfDiscards in the message handler should be: 2
+            assertEquals(2, testMsgHandler.numOfDiscards, "Number of message Discards in the message handler.");
+
+        } catch (SQLException ex) {
+            fail(TestResource.getResource("R_unexpectedErrorMessage") + ex.getMessage());
+        }
+    }
+
+    /**
+     * Test message handler with CallableStatement -- and "feedback" messages
+     * <p>
+     * Do a "long running procedure" and check that the message handler receives the "feedback" messages
+     */
+    @Test
+    @Tag(Constants.xAzureSQLDW)
+    public void testMsgHandlerWithProcedureFeedback() throws Exception {
+        try (SQLServerConnection conn = getConnection()) {
+
+            class TestMsgHandler implements ISQLServerMessageHandler {
+                int numOfCalls = 0;
+                Map<String, Long> feedbackMsgTs = new LinkedHashMap<>();
+
+                @Override
+                public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) {
+                    numOfCalls++;
+
+                    if (50_000 == srvErrorOrWarning.getErrorNumber()) {
+                        // System.out.println("DEBUG: testMsgHandlerWithProcedureFeedback.messageHandler(): FEEDBACK: " + srvErrorOrWarning.getErrorMessage());
+                        // Remember when the message was received
+                        feedbackMsgTs.put(srvErrorOrWarning.getErrorMessage(), System.currentTimeMillis());
+                    }
+
+                    return srvErrorOrWarning;
+                }
+            }
+            TestMsgHandler testMsgHandler = new TestMsgHandler();
+
+            // Create a massage handler
+            conn.setServerMessageHandler(testMsgHandler);
+
+            int doSqlLoopCount = 4;
+            // SQL to create procedure
+            String sqlCreateProc = "" + "CREATE PROCEDURE #msghandler_feeback_proc \n" + "AS \n" + "BEGIN \n"
+                    + "    DECLARE @loop_cnt INT = " + doSqlLoopCount + " \n" + "    DECLARE @feedback VARCHAR(255) \n"
+                    + " \n" + "    WHILE (@loop_cnt > 0) \n" + "    BEGIN \n" + "        WAITFOR DELAY '00:00:01' \n"
+                    + " \n"
+                    + "        SET @feedback = 'In proc, still looping... waiting for loop_count to reach 0. loop_count is now at: ' + convert(varchar(10), @loop_cnt) \n"
+                    + "        RAISERROR(@feedback, 0, 1) WITH NOWAIT \n" + " \n"
+                    + "        SET @loop_cnt = @loop_cnt - 1 \n" + "    END \n" + " \n" + "    RETURN @loop_cnt \n"
+                    + "END \n";
+
+            // Create the proc
+            try (Statement stmnt = conn.createStatement()) {
+                stmnt.executeUpdate(sqlCreateProc);
+                assertEquals(0, getWarningCount("Conn", conn.getWarnings()),
+                        "Expecting NO SQLWarnings from 'create proc', at Connection.");
+                assertEquals(0, getWarningCount("Stmnt", conn.getWarnings()),
+                        "Expecting NO SQLWarnings from 'create proc', at Statement.");
+            }
+
+            // Execute the proc
+            try (CallableStatement cstmnt = conn.prepareCall("{ ? =call  #msghandler_feeback_proc }")) {
+                cstmnt.registerOutParameter(1, Types.INTEGER);
+                cstmnt.execute();
+                int procReturnCode = cstmnt.getInt(1);
+
+                assertEquals(0, procReturnCode, "Unexpected ReturnCode from the temp procedure.");
+
+                assertEquals(0, getWarningCount("conn", conn.getWarnings()),
+                        "Unexpected Number Of SQLWarnings from 'exec proc', at Connection.");
+                assertEquals(doSqlLoopCount, getWarningCount("cstmnt", cstmnt.getWarnings()),
+                        "Unexpected Number Of SQLWarnings from 'exec proc', at CallableStatement.");
+            }
+
+            // numOfCalls to the message handler should be: #
+            assertEquals(doSqlLoopCount, testMsgHandler.numOfCalls, "Number of message calls to the message handler.");
+
+            // Loop all received messages and check that they are within a second (+-200ms)
+            long prevTime = 0;
+            for (Entry<String, Long> entry : testMsgHandler.feedbackMsgTs.entrySet()) {
+                if (prevTime == 0) {
+                    prevTime = entry.getValue();
+                    continue;
+                }
+
+                long msDiff = entry.getValue() - prevTime;
+                if (msDiff < 800 || msDiff > 1200) {
+                    fail("Received Messages is to far apart. They should be approx 1000 ms. msDiff=" + msDiff
+                            + " Message=|" + entry.getKey() + "|.");
+                }
+
+                prevTime = entry.getValue();
+            }
+
+        } catch (SQLException ex) {
+            fail(TestResource.getResource("R_unexpectedErrorMessage") + ex.getMessage());
+        }
+    }
+}
diff --git a/src/test/resources/BulkCopyCSVTestInputDelimiterEscape.csv b/src/test/resources/BulkCopyCSVTestInputDelimiterEscape.csv
index 10afd5d8..9425b0a1 100644
--- a/src/test/resources/BulkCopyCSVTestInputDelimiterEscape.csv
+++ b/src/test/resources/BulkCopyCSVTestInputDelimiterEscape.csv
@@ -9,3 +9,7 @@
 9@1997@Ford@E350@"Super@ ""luxurious"" truck"@ 
 10@1997@ "Ford" @E350@  "E63"@ 
 11@1997@Ford@E350@" Super luxurious truck "@ 
+12@1997@"F
+o
+r
+d"@"E350"@"""Super"" ""luxurious"" ""truck"""@