Repository: microsoft/mssql-jdbc
Pull request #2239 (2023-10-17 17:29:54)

Check for maximum scale in IOBuffer before array copy
Input was not validated for max length before being copied to byte array.


Patch:
diff --git a/README.md b/README.md
index 600d05a5..ffbc0435 100644
--- a/README.md
+++ b/README.md
@@ -79,7 +79,7 @@ We're now on the Maven Central Repository. Add the following to your POM file to
 <dependency>
 	<groupId>com.microsoft.sqlserver</groupId>
 	<artifactId>mssql-jdbc</artifactId>
-	<version>12.4.0.jre11</version>
+	<version>12.4.1.jre11</version>
 </dependency>
 ```
 The driver can be downloaded from [Microsoft](https://aka.ms/downloadmssqljdbc).
@@ -90,7 +90,7 @@ To get the latest version of the driver, add the following to your POM file:
 <dependency>
 	<groupId>com.microsoft.sqlserver</groupId>
 	<artifactId>mssql-jdbc</artifactId>
-	<version>12.4.0.jre11</version>
+	<version>12.4.1.jre11</version>
 </dependency>
 ```
 
@@ -125,7 +125,7 @@ Projects that require either of the two features need to explicitly declare the
 <dependency>
 	<groupId>com.microsoft.sqlserver</groupId>
 	<artifactId>mssql-jdbc</artifactId>
-	<version>12.4.0.jre11</version>
+	<version>12.4.1.jre11</version>
 	<scope>compile</scope>
 </dependency>
 
@@ -143,7 +143,7 @@ Projects that require either of the two features need to explicitly declare the
 <dependency>
 	<groupId>com.microsoft.sqlserver</groupId>
 	<artifactId>mssql-jdbc</artifactId>
-	<version>12.4.0.jre11</version>
+	<version>12.4.1.jre11</version>
 	<scope>compile</scope>
 </dependency>
 
@@ -170,7 +170,7 @@ When setting 'useFmtOnly' property to 'true' for establishing a connection or cr
 <dependency>
 	<groupId>com.microsoft.sqlserver</groupId>
 	<artifactId>mssql-jdbc</artifactId>
-	<version>12.4.0.jre11</version>
+	<version>12.4.1.jre11</version>
 </dependency>
 
 <dependency>
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java b/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java
index 267034bf..c9bc8871 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java
@@ -861,6 +861,9 @@ final class DDC {
      *
      * java.sql.Date java.sql.Time java.sql.Timestamp java.lang.String
      *
+     * @param connection
+     *        the JDBC connection from which value was read
+     *
      * @param jdbcType
      *        the JDBC type indicating the desired conversion
      *
@@ -890,7 +893,8 @@ final class DDC {
      *
      * @return a Java object of the desired type.
      */
-    static final Object convertTemporalToObject(JDBCType jdbcType, SSType ssType, Calendar timeZoneCalendar,
+    static final Object convertTemporalToObject(
+            SQLServerConnection connection, JDBCType jdbcType, SSType ssType, Calendar timeZoneCalendar,
             int daysSinceBaseDate, long ticksSinceMidnight, int fractionalSecondsScale) throws SQLServerException {
 
         // In cases where a Calendar object (and therefore Timezone) is not passed to the method,
@@ -1127,7 +1131,13 @@ final class DDC {
                 java.sql.Timestamp ts2 = new java.sql.Timestamp(cal.getTimeInMillis());
                 ts2.setNanos(subSecondNanos);
                 if (jdbcType == JDBCType.LOCALDATETIME) {
-                    return ts2.toLocalDateTime();
+                    if (connection.getIgnoreOffsetOnDateTimeOffsetConversion()) {
+                        return LocalDateTime.of(
+                                cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH),
+                                cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND), subSecondNanos);
+                    } else {
+                        return ts2.toLocalDateTime();
+                    }
                 }
                 return ts2;
 
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
index bb709307..6a2f5a8d 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
@@ -40,6 +40,7 @@ import java.security.Security;
 import java.sql.Timestamp;
 import java.text.MessageFormat;
 import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.time.OffsetDateTime;
 import java.time.OffsetTime;
 import java.util.ArrayList;
@@ -3755,29 +3756,24 @@ final class TDSWriter {
         writeShort((short) minutesSinceMidnight);
     }
 
-    void writeDatetime(String value) throws SQLServerException {
-        GregorianCalendar calendar = initializeCalender(TimeZone.getDefault());
-        long utcMillis; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT)
+    void writeDatetime(java.sql.Timestamp dateValue) throws SQLServerException {
+        LocalDateTime ldt = dateValue.toLocalDateTime();
         int subSecondNanos;
-        java.sql.Timestamp timestampValue = java.sql.Timestamp.valueOf(value);
-        utcMillis = timestampValue.getTime();
-        subSecondNanos = timestampValue.getNanos();
+        subSecondNanos = ldt.getNano();
 
-        // Load the calendar with the desired value
-        calendar.setTimeInMillis(utcMillis);
 
         // Number of days there have been since the SQL Base Date.
         // These are based on SQL Server algorithms
-        int daysSinceSQLBaseDate = DDC.daysSinceBaseDate(calendar.get(Calendar.YEAR),
-                calendar.get(Calendar.DAY_OF_YEAR), TDS.BASE_YEAR_1900);
+        int daysSinceSQLBaseDate = DDC.daysSinceBaseDate(ldt.getYear(),
+                ldt.getDayOfYear(), TDS.BASE_YEAR_1900);
 
         // Number of milliseconds since midnight of the current day.
         int millisSinceMidnight = (subSecondNanos + Nanos.PER_MILLISECOND / 2) / Nanos.PER_MILLISECOND + // Millis into
-                                                                                                         // the current
-                                                                                                         // second
-                1000 * calendar.get(Calendar.SECOND) + // Seconds into the current minute
-                60 * 1000 * calendar.get(Calendar.MINUTE) + // Minutes into the current hour
-                60 * 60 * 1000 * calendar.get(Calendar.HOUR_OF_DAY); // Hours into the current day
+                // the current
+                // second
+                1000 * ldt.getSecond() + // Seconds into the current minute
+                60 * 1000 * ldt.getMinute() + // Minutes into the current hour
+                60 * 60 * 1000 * ldt.getHour(); // Hours into the current day
 
         // The last millisecond of the current day is always rounded to the first millisecond
         // of the next day because DATETIME is only accurate to 1/300th of a second.
@@ -5032,7 +5028,8 @@ final class TDSWriter {
     }
 
     private void writeInternalTVPRowValues(JDBCType jdbcType, String currentColumnStringValue, Object currentObject,
-            Map.Entry<Integer, SQLServerMetaData> columnPair, boolean isSqlVariant) throws SQLServerException {
+            Map.Entry<Integer, SQLServerMetaData> columnPair, boolean isSqlVariant)
+                    throws SQLServerException, IllegalArgumentException {
         boolean isShortValue, isNull;
         int dataLength;
         switch (jdbcType) {
@@ -5104,9 +5101,18 @@ final class TDSWriter {
 
                     /*
                      * setScale of all BigDecimal value based on metadata as scale is not sent separately for individual
-                     * value. Use the rounding used in Server. Say, for BigDecimal("0.1"), if scale in metdadata is 0,
+                     * value. Use the rounding used in Server. Say, for BigDecimal("0.1"), if scale in metadata is 0,
                      * then ArithmeticException would be thrown if RoundingMode is not set
+                     *
+                     * Additionally, we should check here if the scale is within the bounds of SQLServer as it is
+                     * possible for a number with a scale larger than 38 to be passed in.
                      */
+                    if (columnPair.getValue().scale > SQLServerConnection.MAX_DECIMAL_PRECISION) {
+                        MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidScale"));
+                        Object[] msgArgs = {columnPair.getValue().scale};
+                        throw new IllegalArgumentException(form.format(msgArgs));
+                    }
+
                     bdValue = bdValue.setScale(columnPair.getValue().scale, RoundingMode.HALF_UP);
 
                     byte[] val = DDC.convertBigDecimalToBytes(bdValue, bdValue.scale());
@@ -7229,7 +7235,7 @@ final class TDSReader implements Serializable {
         }
 
         // Convert the DATETIME/SMALLDATETIME value to the desired Java type.
-        return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME, appTimeZoneCalendar, daysSinceSQLBaseDate,
+        return DDC.convertTemporalToObject(con, jdbcType, SSType.DATETIME, appTimeZoneCalendar, daysSinceSQLBaseDate,
                 msecSinceMidnight, 0); // scale
                                        // (ignored
                                        // for
@@ -7246,7 +7252,7 @@ final class TDSReader implements Serializable {
         int localDaysIntoCE = readDaysIntoCE();
 
         // Convert the DATE value to the desired Java type.
-        return DDC.convertTemporalToObject(jdbcType, SSType.DATE, appTimeZoneCalendar, localDaysIntoCE, 0, // midnight
+        return DDC.convertTemporalToObject(con, jdbcType, SSType.DATE, appTimeZoneCalendar, localDaysIntoCE, 0, // midnight
                                                                                                            // local to
                                                                                                            // app time
                                                                                                            // zone
@@ -7262,7 +7268,7 @@ final class TDSReader implements Serializable {
         long localNanosSinceMidnight = readNanosSinceMidnight(typeInfo.getScale());
 
         // Convert the TIME value to the desired Java type.
-        return DDC.convertTemporalToObject(jdbcType, SSType.TIME, appTimeZoneCalendar, 0, localNanosSinceMidnight,
+        return DDC.convertTemporalToObject(con, jdbcType, SSType.TIME, appTimeZoneCalendar, 0, localNanosSinceMidnight,
                 typeInfo.getScale());
     }
 
@@ -7276,7 +7282,7 @@ final class TDSReader implements Serializable {
         int localDaysIntoCE = readDaysIntoCE();
 
         // Convert the DATETIME2 value to the desired Java type.
-        return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME2, appTimeZoneCalendar, localDaysIntoCE,
+        return DDC.convertTemporalToObject(con, jdbcType, SSType.DATETIME2, appTimeZoneCalendar, localDaysIntoCE,
                 localNanosSinceMidnight, typeInfo.getScale());
     }
 
@@ -7291,7 +7297,7 @@ final class TDSReader implements Serializable {
         int localMinutesOffset = readShort();
 
         // Convert the DATETIMEOFFSET value to the desired Java type.
-        return DDC.convertTemporalToObject(jdbcType, SSType.DATETIMEOFFSET,
+        return DDC.convertTemporalToObject(con, jdbcType, SSType.DATETIMEOFFSET,
                 new GregorianCalendar(new SimpleTimeZone(localMinutesOffset * 60 * 1000, ""), Locale.US), utcDaysIntoCE,
                 utcNanosSinceMidnight, typeInfo.getScale());
     }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
index 88fa73a3..8025410c 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
@@ -416,6 +416,21 @@ public interface ISQLServerConnection extends java.sql.Connection {
      */
     void setDelayLoadingLobs(boolean delayLoadingLobs);
 
+    /**
+     * Returns the current flag value for ignoreOffsetOnDateTimeOffsetConversion.
+     *
+     * @return 'ignoreOffsetOnDateTimeOffsetConversion' property value.
+     */
+    boolean getIgnoreOffsetOnDateTimeOffsetConversion();
+
+    /**
+     * Specifies the flag to ignore offset when converting DATETIMEOFFSET to LocalDateTime.
+     *
+     * @param ignoreOffsetOnDateTimeOffsetConversion
+     *        boolean value for 'ignoreOffsetOnDateTimeOffsetConversion'.
+     */
+    void setIgnoreOffsetOnDateTimeOffsetConversion(boolean ignoreOffsetOnDateTimeOffsetConversion);
+
     /**
      * Sets the name of the preferred type of IP Address.
      * 
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java
index afadf944..427fe916 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java
@@ -2463,7 +2463,12 @@ public class SQLServerBulkCopy implements java.lang.AutoCloseable, java.io.Seria
                             case DATETIME:
                                 if (bulkNullable)
                                     tdsWriter.writeByte((byte) 0x08);
-                                tdsWriter.writeDatetime(colValue.toString());
+
+                                if (colValue instanceof java.sql.Timestamp) {
+                                    tdsWriter.writeDatetime((java.sql.Timestamp) colValue);
+                                } else {
+                                    tdsWriter.writeDatetime(java.sql.Timestamp.valueOf(colValue.toString()));
+                                }
                                 break;
                             default: // DATETIME2
                                 if (2 >= bulkScale)
@@ -2697,15 +2702,15 @@ public class SQLServerBulkCopy implements java.lang.AutoCloseable, java.io.Seria
                 tdsWriter.writeTime((java.sql.Timestamp) colValue, timeBulkScale);
                 break;
 
-            case DATETIME8:
-                writeBulkCopySqlVariantHeader(10, TDSType.DATETIME8.byteValue(), (byte) 0, tdsWriter);
-                tdsWriter.writeDatetime(colValue.toString());
-                break;
-
             case DATETIME4:
                 // when the type is ambiguous, we write to bigger type
+            case DATETIME8:
                 writeBulkCopySqlVariantHeader(10, TDSType.DATETIME8.byteValue(), (byte) 0, tdsWriter);
-                tdsWriter.writeDatetime(colValue.toString());
+                if (colValue instanceof java.sql.Timestamp) {
+                    tdsWriter.writeDatetime((java.sql.Timestamp) colValue);
+                } else {
+                    tdsWriter.writeDatetime(java.sql.Timestamp.valueOf(colValue.toString()));
+                }
                 break;
 
             case DATETIME2N:
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
index 66ad92b9..acdf6723 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
@@ -973,6 +973,19 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         delayLoadingLobs = b;
     }
 
+    /** Boolean that indicates whether datetime types are converted to java.time objects using java.time rules */
+    private boolean ignoreOffsetOnDateTimeOffsetConversion = SQLServerDriverBooleanProperty.IGNORE_OFFSET_ON_DATE_TIME_OFFSET_CONVERSION.getDefaultValue();
+
+    @Override
+    public boolean getIgnoreOffsetOnDateTimeOffsetConversion() {
+        return ignoreOffsetOnDateTimeOffsetConversion;
+    }
+
+    @Override
+    public void setIgnoreOffsetOnDateTimeOffsetConversion(boolean ignoreOffsetOnDateTimeOffsetConversion) {
+        this.ignoreOffsetOnDateTimeOffsetConversion = ignoreOffsetOnDateTimeOffsetConversion;
+    }
+
     /** Session Recovery Object */
     private transient IdleConnectionResiliency sessionRecovery = new IdleConnectionResiliency(this);
 
@@ -2935,6 +2948,14 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                 }
                 delayLoadingLobs = isBooleanPropertyOn(sPropKey, sPropValue);
 
+                sPropKey = SQLServerDriverBooleanProperty.IGNORE_OFFSET_ON_DATE_TIME_OFFSET_CONVERSION.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.IGNORE_OFFSET_ON_DATE_TIME_OFFSET_CONVERSION.getDefaultValue());
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                }
+                ignoreOffsetOnDateTimeOffsetConversion = isBooleanPropertyOn(sPropKey, sPropValue);
+
                 FailoverInfo fo = null;
                 String databaseNameProperty = SQLServerDriverStringProperty.DATABASE_NAME.toString();
                 String serverNameProperty = SQLServerDriverStringProperty.SERVER_NAME.toString();
@@ -7288,6 +7309,9 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
 
     /** original delayLoadingLobs */
     private boolean originalDelayLoadingLobs;
+    
+    /** original ignoreOffsetOnDateTimeOffsetConversion */
+    private boolean originalIgnoreOffsetOnDateTimeOffsetConversion;
 
     /** Always Encrypted version */
     private int aeVersion = TDS.COLUMNENCRYPTION_NOT_SUPPORTED;
@@ -7313,6 +7337,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                 openStatements = new LinkedList<>();
                 originalUseFmtOnly = useFmtOnly;
                 originalDelayLoadingLobs = delayLoadingLobs;
+                originalIgnoreOffsetOnDateTimeOffsetConversion = ignoreOffsetOnDateTimeOffsetConversion;
                 requestStarted = true;
             }
         } finally {
@@ -7371,6 +7396,9 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                 if (delayLoadingLobs != originalDelayLoadingLobs) {
                     setDelayLoadingLobs(originalDelayLoadingLobs);
                 }
+                if (ignoreOffsetOnDateTimeOffsetConversion != originalIgnoreOffsetOnDateTimeOffsetConversion) {
+                    setIgnoreOffsetOnDateTimeOffsetConversion(originalIgnoreOffsetOnDateTimeOffsetConversion);
+                }
                 sqlWarnings = originalSqlWarnings;
                 if (null != openStatements) {
                     while (!openStatements.isEmpty()) {
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
index 16864435..248957b3 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
@@ -632,6 +632,16 @@ class SQLServerConnectionPoolProxy implements ISQLServerConnection, java.io.Seri
         wrappedConnection.setDelayLoadingLobs(delayLoadingLobs);
     }
 
+    @Override
+    public boolean getIgnoreOffsetOnDateTimeOffsetConversion() {
+        return wrappedConnection.getIgnoreOffsetOnDateTimeOffsetConversion();
+    }
+
+    @Override
+    public void setIgnoreOffsetOnDateTimeOffsetConversion(boolean ignoreOffsetOnDateTimeOffsetConversion) {
+        wrappedConnection.setIgnoreOffsetOnDateTimeOffsetConversion(ignoreOffsetOnDateTimeOffsetConversion);
+    }
+
     @Override
     public void setIPAddressPreference(String iPAddressPreference) {
         wrappedConnection.setIPAddressPreference(iPAddressPreference);
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
index fc4b1f73..d5f40111 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
@@ -694,6 +694,7 @@ enum SQLServerDriverBooleanProperty {
     USE_FMT_ONLY("useFmtOnly", false),
     SEND_TEMPORAL_DATATYPES_AS_STRING_FOR_BULK_COPY("sendTemporalDataTypesAsStringForBulkCopy", true),
     DELAY_LOADING_LOBS("delayLoadingLobs", true),
+    IGNORE_OFFSET_ON_DATE_TIME_OFFSET_CONVERSION("ignoreOffsetOnDateTimeOffsetConversion", false),
     USE_DEFAULT_JAAS_CONFIG("useDefaultJaasConfig", false),
     USE_DEFAULT_GSS_CREDENTIAL("useDefaultGSSCredential", false);
 
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
index effb8ae6..0b4e3628 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
@@ -540,7 +540,8 @@ public final class SQLServerResource extends ListResourceBundle {
         {"R_serverError", "An error occurred during the current command (Done status {0}). {1}"},
         {"R_ManagedIdentityTokenAcquisitionFail", "Failed to acquire managed identity token. Request for the token succeeded, but no token was returned. The token is null."},
         {"R_AmbiguousRowUpdate", "Failed to execute updateRow(). The update is attempting an ambiguous update on tables \"{0}\" and \"{1}\". Ensure all columns being updated prior to the updateRow() call belong to the same table."},
-        {"R_InvalidSqlQuery", "Invalid SQL Query: {0}"}
+        {"R_InvalidSqlQuery", "Invalid SQL Query: {0}"},
+        {"R_InvalidScale", "Scale of input value is larger than the maximum allowed by SQL Server."}
     };
 }
 // @formatter:on
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java
index 7fd3ba44..ab76347b 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java
@@ -3543,13 +3543,13 @@ final class ServerDTVImpl extends DTVImpl {
                 // cannot reuse method
                 int daysIntoCE = getDaysIntoCE(decryptedValue, baseSSType);
 
-                return DDC.convertTemporalToObject(jdbcType, baseSSType, cal, daysIntoCE, 0, 0);
+                return DDC.convertTemporalToObject(con, jdbcType, baseSSType, cal, daysIntoCE, 0, 0);
 
             case TIME:
                 long localNanosSinceMidnight = readNanosSinceMidnightAE(decryptedValue, baseTypeInfo.getScale(),
                         baseSSType);
 
-                return DDC.convertTemporalToObject(jdbcType, SSType.TIME, cal, 0, localNanosSinceMidnight,
+                return DDC.convertTemporalToObject(con, jdbcType, SSType.TIME, cal, 0, localNanosSinceMidnight,
                         baseTypeInfo.getScale());
 
             case DATETIME2:
@@ -3570,7 +3570,7 @@ final class ServerDTVImpl extends DTVImpl {
                 int daysIntoCE2 = getDaysIntoCE(datePortion, baseSSType);
 
                 // Convert the DATETIME2 value to the desired Java type.
-                return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME2, cal, daysIntoCE2,
+                return DDC.convertTemporalToObject(con, jdbcType, SSType.DATETIME2, cal, daysIntoCE2,
                         localNanosSinceMidnight2, baseTypeInfo.getScale());
 
             case SMALLDATETIME:
@@ -3582,7 +3582,7 @@ final class ServerDTVImpl extends DTVImpl {
                 // SQL smalldatetime has less precision. It stores 2 bytes
                 // for the days since SQL Base Date and 2 bytes for minutes
                 // after midnight.
-                return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME, cal,
+                return DDC.convertTemporalToObject(con, jdbcType, SSType.DATETIME, cal,
                         Util.readUnsignedShort(decryptedValue, 0),
                         Util.readUnsignedShort(decryptedValue, 2) * 60L * 1000L, 0);
 
@@ -3597,7 +3597,7 @@ final class ServerDTVImpl extends DTVImpl {
                 // SQL datetime is 4 bytes for days since SQL Base Date
                 // (January 1, 1900 00:00:00 GMT) and 4 bytes for
                 // the number of three hundredths (1/300) of a second since midnight.
-                return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME, cal, Util.readInt(decryptedValue, 0),
+                return DDC.convertTemporalToObject(con, jdbcType, SSType.DATETIME, cal, Util.readInt(decryptedValue, 0),
                         ticksSinceMidnight, 0);
 
             case DATETIMEOFFSET:
@@ -3616,7 +3616,7 @@ final class ServerDTVImpl extends DTVImpl {
 
                 int localMinutesOffset = ByteBuffer.wrap(offsetPortion2).order(ByteOrder.LITTLE_ENDIAN).getShort();
 
-                return DDC.convertTemporalToObject(jdbcType, SSType.DATETIMEOFFSET,
+                return DDC.convertTemporalToObject(con, jdbcType, SSType.DATETIMEOFFSET,
                         new GregorianCalendar(new SimpleTimeZone(localMinutesOffset * 60 * 1000, ""), Locale.US),
                         daysIntoCE3, localNanosSinceMidnight3, baseTypeInfo.getScale());
 


Test patch:
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyAllTypesTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyAllTypesTest.java
index 11d88d61..a7e69b01 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyAllTypesTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyAllTypesTest.java
@@ -8,6 +8,8 @@ import java.sql.Connection;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Statement;
+import java.sql.Timestamp;
+import java.sql.Types;
 
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Tag;
@@ -16,8 +18,12 @@ import org.junit.platform.runner.JUnitPlatform;
 import org.junit.runner.RunWith;
 
 import com.microsoft.sqlserver.jdbc.ComparisonUtil;
+import com.microsoft.sqlserver.jdbc.RandomData;
+import com.microsoft.sqlserver.jdbc.RandomUtil;
 import com.microsoft.sqlserver.jdbc.SQLServerBulkCopy;
+import com.microsoft.sqlserver.jdbc.SQLServerBulkCopyOptions;
 import com.microsoft.sqlserver.jdbc.TestUtils;
+import com.microsoft.sqlserver.testframework.AbstractSQLGenerator;
 import com.microsoft.sqlserver.testframework.AbstractTest;
 import com.microsoft.sqlserver.testframework.Constants;
 import com.microsoft.sqlserver.testframework.DBConnection;
@@ -25,6 +31,20 @@ import com.microsoft.sqlserver.testframework.DBStatement;
 import com.microsoft.sqlserver.testframework.DBTable;
 import com.microsoft.sqlserver.testframework.PrepUtil;
 
+import javax.sql.RowSetMetaData;
+import javax.sql.rowset.CachedRowSet;
+import javax.sql.rowset.RowSetFactory;
+import javax.sql.rowset.RowSetMetaDataImpl;
+import javax.sql.rowset.RowSetProvider;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
 
 @RunWith(JUnitPlatform.class)
 public class BulkCopyAllTypesTest extends AbstractTest {
@@ -98,6 +118,71 @@ public class BulkCopyAllTypesTest extends AbstractTest {
         try (Statement stmt = connection.createStatement()) {
             TestUtils.dropTableIfExists(tableSrc.getEscapedTableName(), stmt);
             TestUtils.dropTableIfExists(tableDest.getEscapedTableName(), stmt);
+            TestUtils.dropTableIfExists(dateTimeTestTable, stmt);
+        }
+    }
+
+    private static final int DATETIME_COL_COUNT = 2;
+    private static final int DATETIME_ROW_COUNT = 1;
+    private static final String dateTimeTestTable =
+            AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("bulkCopyTimestampTest"));
+
+    @Test
+    public void testBulkCopyTimestamp() throws SQLException {
+        List<Timestamp> timeStamps = new ArrayList<>();
+        try (Connection con = getConnection(); Statement stmt = connection.createStatement()) {
+            String colSpec = IntStream.range(1, DATETIME_COL_COUNT + 1).mapToObj(x -> String.format("c%d datetime", x)).collect(
+                    Collectors.joining(","));
+            String sql1 = String.format("create table %s (%s)", dateTimeTestTable, colSpec);
+            stmt.execute(sql1);
+
+            RowSetFactory rsf = RowSetProvider.newFactory();
+            CachedRowSet crs = rsf.createCachedRowSet();
+            RowSetMetaData rsmd = new RowSetMetaDataImpl();
+            rsmd.setColumnCount(DATETIME_COL_COUNT);
+
+            for (int i = 1; i <= DATETIME_COL_COUNT; i++) {
+                rsmd.setColumnName(i, String.format("c%d", i));
+                rsmd.setColumnType(i, Types.TIMESTAMP);
+            }
+            crs.setMetaData(rsmd);
+
+            for (int i = 0; i < DATETIME_COL_COUNT; i++) {
+                timeStamps.add(RandomData.generateDatetime(false));
+            }
+
+            for (int ri = 0; ri < DATETIME_ROW_COUNT; ri++) {
+                crs.moveToInsertRow();
+
+                for (int i = 1; i <= DATETIME_COL_COUNT; i++) {
+                        crs.updateTimestamp(i, timeStamps.get(i - 1));
+                }
+                crs.insertRow();
+            }
+            crs.moveToCurrentRow();
+
+            try (SQLServerBulkCopy bcOperation = new SQLServerBulkCopy(con)) {
+                SQLServerBulkCopyOptions bcOptions = new SQLServerBulkCopyOptions();
+                bcOptions.setBatchSize(5000);
+                bcOperation.setDestinationTableName(dateTimeTestTable);
+                bcOperation.setBulkCopyOptions(bcOptions);
+                bcOperation.writeToServer(crs);
+            }
+
+            try (ResultSet rs = stmt.executeQuery("select * from " + dateTimeTestTable)) {
+                assertTrue(rs.next());
+
+                for (int i = 1; i <= DATETIME_COL_COUNT; i++) {
+                    long expectedTimestamp = getTime(timeStamps.get(i - 1));
+                    long actualTimestamp = getTime(rs.getTimestamp(i));
+
+                    assertEquals(expectedTimestamp, actualTimestamp);
+                }
+            }
         }
     }
+
+    private static long getTime(Timestamp time) {
+        return (3 * time.getTime() + 5) / 10;
+    }
 }
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 d0ce2556..da2d4d73 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java
@@ -74,6 +74,7 @@ public class RequestBoundaryMethodsTest extends AbstractTest {
         boolean useBulkCopyForBatchInsert1 = true;
         boolean useFmtOnly1 = true;
         boolean delayLoadingLobs1 = false;
+        boolean ignoreOffsetOnDateTimeOffsetConversion1 = true;
 
         boolean autoCommitMode2 = false;
         int transactionIsolationLevel2 = SQLServerConnection.TRANSACTION_SERIALIZABLE;
@@ -88,6 +89,7 @@ public class RequestBoundaryMethodsTest extends AbstractTest {
         boolean useBulkCopyForBatchInsert2 = false;
         boolean useFmtOnly2 = false;
         boolean delayLoadingLobs2 = true;
+        boolean ignoreOffsetOnDateTimeOffsetConversion2 = false;
 
         try (SQLServerConnection con = getConnection(); Statement stmt = con.createStatement()) {
             if (TestUtils.isJDBC43OrGreater(con)) {
@@ -98,53 +100,53 @@ public class RequestBoundaryMethodsTest extends AbstractTest {
                 setConnectionFields(con, autoCommitMode1, transactionIsolationLevel1, networkTimeout1, holdability1,
                         sendTimeAsDatetime1, statementPoolingCacheSize1, disableStatementPooling1,
                         serverPreparedStatementDiscardThreshold1, enablePrepareOnFirstPreparedStatementCall1, sCatalog1,
-                        useBulkCopyForBatchInsert1, useFmtOnly1, delayLoadingLobs1);
+                        useBulkCopyForBatchInsert1, useFmtOnly1, delayLoadingLobs1, ignoreOffsetOnDateTimeOffsetConversion1);
                 con.beginRequest();
                 // Call setters with the second set of values inside beginRequest()/endRequest() block.
                 setConnectionFields(con, autoCommitMode2, transactionIsolationLevel2, networkTimeout2, holdability2,
                         sendTimeAsDatetime2, statementPoolingCacheSize2, disableStatementPooling2,
                         serverPreparedStatementDiscardThreshold2, enablePrepareOnFirstPreparedStatementCall2, sCatalog2,
-                        useBulkCopyForBatchInsert2, useFmtOnly2, delayLoadingLobs2);
+                        useBulkCopyForBatchInsert2, useFmtOnly2, delayLoadingLobs2, ignoreOffsetOnDateTimeOffsetConversion2);
                 con.endRequest();
                 // Test if endRequest() resets the SQLServerConnection properties back to the first set of values.
                 compareValuesAgainstConnection(con, autoCommitMode1, transactionIsolationLevel1, networkTimeout1,
                         holdability1, sendTimeAsDatetime1, statementPoolingCacheSize1, disableStatementPooling1,
                         serverPreparedStatementDiscardThreshold1, enablePrepareOnFirstPreparedStatementCall1, sCatalog1,
-                        useBulkCopyForBatchInsert1, useFmtOnly1, delayLoadingLobs1);
+                        useBulkCopyForBatchInsert1, useFmtOnly1, delayLoadingLobs1, ignoreOffsetOnDateTimeOffsetConversion1);
 
                 // Multiple calls to beginRequest() without an intervening call to endRequest() are no-op.
                 setConnectionFields(con, autoCommitMode2, transactionIsolationLevel2, networkTimeout2, holdability2,
                         sendTimeAsDatetime2, statementPoolingCacheSize2, disableStatementPooling2,
                         serverPreparedStatementDiscardThreshold2, enablePrepareOnFirstPreparedStatementCall2, sCatalog2,
-                        useBulkCopyForBatchInsert2, useFmtOnly2, delayLoadingLobs2);
+                        useBulkCopyForBatchInsert2, useFmtOnly2, delayLoadingLobs2, ignoreOffsetOnDateTimeOffsetConversion2);
                 con.beginRequest();
                 setConnectionFields(con, autoCommitMode1, transactionIsolationLevel1, networkTimeout1, holdability1,
                         sendTimeAsDatetime1, statementPoolingCacheSize1, disableStatementPooling1,
                         serverPreparedStatementDiscardThreshold1, enablePrepareOnFirstPreparedStatementCall1, sCatalog1,
-                        useBulkCopyForBatchInsert1, useFmtOnly1, delayLoadingLobs1);
+                        useBulkCopyForBatchInsert1, useFmtOnly1, delayLoadingLobs1, ignoreOffsetOnDateTimeOffsetConversion1);
                 con.beginRequest();
                 con.endRequest();
                 // Same values as before the first beginRequest()
                 compareValuesAgainstConnection(con, autoCommitMode2, transactionIsolationLevel2, networkTimeout2,
                         holdability2, sendTimeAsDatetime2, statementPoolingCacheSize2, disableStatementPooling2,
                         serverPreparedStatementDiscardThreshold2, enablePrepareOnFirstPreparedStatementCall2, sCatalog2,
-                        useBulkCopyForBatchInsert2, useFmtOnly2, delayLoadingLobs2);
+                        useBulkCopyForBatchInsert2, useFmtOnly2, delayLoadingLobs2, ignoreOffsetOnDateTimeOffsetConversion2);
 
                 // A call to endRequest() without an intervening call to beginRequest() is no-op.
                 setConnectionFields(con, autoCommitMode1, transactionIsolationLevel1, networkTimeout1, holdability1,
                         sendTimeAsDatetime1, statementPoolingCacheSize1, disableStatementPooling1,
                         serverPreparedStatementDiscardThreshold1, enablePrepareOnFirstPreparedStatementCall1, sCatalog1,
-                        useBulkCopyForBatchInsert1, useFmtOnly1, delayLoadingLobs1);
+                        useBulkCopyForBatchInsert1, useFmtOnly1, delayLoadingLobs1, ignoreOffsetOnDateTimeOffsetConversion1);
                 setConnectionFields(con, autoCommitMode2, transactionIsolationLevel2, networkTimeout2, holdability2,
                         sendTimeAsDatetime2, statementPoolingCacheSize2, disableStatementPooling2,
                         serverPreparedStatementDiscardThreshold2, enablePrepareOnFirstPreparedStatementCall2, sCatalog2,
-                        useBulkCopyForBatchInsert2, useFmtOnly2, delayLoadingLobs2);
+                        useBulkCopyForBatchInsert2, useFmtOnly2, delayLoadingLobs2, ignoreOffsetOnDateTimeOffsetConversion2);
                 con.endRequest();
                 // No change.
                 compareValuesAgainstConnection(con, autoCommitMode2, transactionIsolationLevel2, networkTimeout2,
                         holdability2, sendTimeAsDatetime2, statementPoolingCacheSize2, disableStatementPooling2,
                         serverPreparedStatementDiscardThreshold2, enablePrepareOnFirstPreparedStatementCall2, sCatalog2,
-                        useBulkCopyForBatchInsert2, useFmtOnly2, delayLoadingLobs2);
+                        useBulkCopyForBatchInsert2, useFmtOnly2, delayLoadingLobs2, ignoreOffsetOnDateTimeOffsetConversion2);
             }
         } finally {
             TestUtils.dropDatabaseIfExists(sCatalog2, connectionString);
@@ -397,7 +399,7 @@ public class RequestBoundaryMethodsTest extends AbstractTest {
             int networkTimeout, int holdability, boolean sendTimeAsDatetime, int statementPoolingCacheSize,
             boolean disableStatementPooling, int serverPreparedStatementDiscardThreshold,
             boolean enablePrepareOnFirstPreparedStatementCall, String sCatalog, boolean useBulkCopyForBatchInsert,
-            boolean useFmtOnly, boolean delayLoadingLobs) throws SQLException {
+            boolean useFmtOnly, boolean delayLoadingLobs, boolean ignoreOffsetOnDateTimeOffsetConversion) throws SQLException {
         con.setAutoCommit(autoCommitMode);
         con.setTransactionIsolation(transactionIsolationLevel);
         con.setNetworkTimeout(null, networkTimeout);
@@ -411,13 +413,14 @@ public class RequestBoundaryMethodsTest extends AbstractTest {
         con.setUseBulkCopyForBatchInsert(useBulkCopyForBatchInsert);
         con.setUseFmtOnly(useFmtOnly);
         con.setDelayLoadingLobs(delayLoadingLobs);
+        con.setIgnoreOffsetOnDateTimeOffsetConversion(ignoreOffsetOnDateTimeOffsetConversion);
     }
 
     private void compareValuesAgainstConnection(SQLServerConnection con, boolean autoCommitMode,
             int transactionIsolationLevel, int networkTimeout, int holdability, boolean sendTimeAsDatetime,
             int statementPoolingCacheSize, boolean disableStatementPooling, int serverPreparedStatementDiscardThreshold,
             boolean enablePrepareOnFirstPreparedStatementCall, String sCatalog, boolean useBulkCopyForBatchInsert,
-            boolean useFmtOnly, boolean delayLoadingLobs) throws SQLException {
+            boolean useFmtOnly, boolean delayLoadingLobs, boolean ignoreOffsetOnDateTimeOffsetConversion) throws SQLException {
         final String description = " values do not match.";
         assertEquals(autoCommitMode, con.getAutoCommit(), "autoCommitmode" + description);
         assertEquals(transactionIsolationLevel, con.getTransactionIsolation(),
@@ -438,6 +441,7 @@ public class RequestBoundaryMethodsTest extends AbstractTest {
                 "useBulkCopyForBatchInsert" + description);
         assertEquals(useFmtOnly, con.getUseFmtOnly(), "useFmtOnly" + description);
         assertEquals(delayLoadingLobs, con.getDelayLoadingLobs(), "delayLoadingLobs" + description);
+        assertEquals(ignoreOffsetOnDateTimeOffsetConversion, con.getIgnoreOffsetOnDateTimeOffsetConversion(), "ignoreOffsetOnDateTimeOffsetConversion" + description);
     }
 
     private void generateWarning(Connection con) throws SQLException {
@@ -495,6 +499,7 @@ public class RequestBoundaryMethodsTest extends AbstractTest {
         verifiedMethodNames.add("createArrayOf");
         verifiedMethodNames.add("setUseFmtOnly");
         verifiedMethodNames.add("setDelayLoadingLobs");
+        verifiedMethodNames.add("setIgnoreOffsetOnDateTimeOffsetConversion");
         verifiedMethodNames.add("registerColumnEncryptionKeyStoreProvidersOnConnection");
         verifiedMethodNames.add("getPrepareMethod");
         verifiedMethodNames.add("setPrepareMethod");
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/DataTypesTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/DataTypesTest.java
index 5c6a5acc..de8e160a 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/DataTypesTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/DataTypesTest.java
@@ -22,7 +22,9 @@ import java.time.Instant;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
+import java.time.OffsetDateTime;
 import java.time.ZoneId;
+import java.time.ZoneOffset;
 import java.time.temporal.ChronoUnit;
 import java.time.zone.ZoneOffsetTransition;
 import java.time.zone.ZoneRules;
@@ -1872,6 +1874,47 @@ public class DataTypesTest extends AbstractTest {
         }
     }
 
+    @Test
+    public void testGetLocalDateTimeTypes() throws Exception {
+        // test value needs to be in a time zone other than local
+        OffsetDateTime value = OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS); // Linux has more precision than SQL Server
+        int offsetSeconds = value.getOffset().getTotalSeconds();
+        offsetSeconds += offsetSeconds < 0 ? 3600 : -3600;
+        value = value.withOffsetSameLocal(ZoneOffset.ofTotalSeconds(offsetSeconds));
+        LocalDateTime valueWithOffsetConversion = value.atZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime();
+
+        try (SQLServerConnection conn = PrepUtil.getConnection(connectionString)) {
+            try (PreparedStatement stmt = conn.prepareStatement("SELECT ?")) {
+                stmt.setObject(1, value);
+
+                ResultSet rs = stmt.executeQuery();
+                rs.next();
+
+                // default behavior is to apply the time zone offset when converting DATETIMEOFFSET to local java.time types
+                assertEquals(value, rs.getObject(1, OffsetDateTime.class));
+                assertEquals(valueWithOffsetConversion, rs.getObject(1, LocalDateTime.class));
+                assertEquals(valueWithOffsetConversion.toLocalDate(), rs.getObject(1, LocalDate.class));
+                assertEquals(valueWithOffsetConversion.toLocalTime(), rs.getObject(1, LocalTime.class));
+            }
+            
+            // change the behavior to be compatible with java.time conversion methods
+            conn.setIgnoreOffsetOnDateTimeOffsetConversion(true);
+            
+            try (PreparedStatement stmt = conn.prepareStatement("SELECT ?")) {
+                stmt.setObject(1, value);
+
+                ResultSet rs = stmt.executeQuery();
+                rs.next();
+
+                // now the offset should be ignored instead of converting to local time zone
+                assertEquals(value, rs.getObject(1, OffsetDateTime.class));
+                assertEquals(value.toLocalDateTime(), rs.getObject(1, LocalDateTime.class));
+                assertEquals(value.toLocalDate(), rs.getObject(1, LocalDate.class));
+                assertEquals(value.toLocalTime(), rs.getObject(1, LocalTime.class));
+            }
+        }
+    }
+
     /**
      * Test example from https://github.com/microsoft/mssql-jdbc/issues/1143
      * 
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPTypesTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPTypesTest.java
index e56ec220..5239d2ff 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPTypesTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPTypesTest.java
@@ -8,6 +8,7 @@ import static org.junit.Assert.fail;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.math.BigDecimal;
 import java.sql.Connection;
 import java.sql.ResultSet;
 import java.sql.SQLException;
@@ -573,6 +574,33 @@ public class TVPTypesTest extends AbstractTest {
         }
     }
 
+    /**
+     * Numeric (bigdecimal) with StoredProcedure
+     *
+     * @throws SQLException
+     */
+    @Test
+    public void testTVPNumericStoredProcedure() throws SQLException {
+        createTables("numeric(10,2)");
+        createTVPS("numeric(38,10)");
+        createProcedure();
+
+        tvp = new SQLServerDataTable();
+        tvp.addColumnMetadata("c1", java.sql.Types.NUMERIC);
+        tvp.addRow(new BigDecimal(0.222));
+
+        final String sql = "{call " + AbstractSQLGenerator.escapeIdentifier(procedureName) + "(?)}";
+
+        try (SQLServerCallableStatement callableStmt = (SQLServerCallableStatement) connection.prepareCall(sql)) {
+            callableStmt.setStructured(1, tvpName, tvp);
+            callableStmt.execute();
+
+            fail(TestResource.getResource("R_expectedExceptionNotThrown"));
+        } catch (IllegalArgumentException e) {
+            assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidScale")), e.getMessage());
+        }
+    }
+
     /**
      * Negative test cases for testing Boolean with TVP
      *