diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java b/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java index 267034bfe..c9bc88718 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 @@ private static String fractionalSecondsString(long subSecondNanos, int scale) { * * 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 @@ private static String fractionalSecondsString(long subSecondNanos, int scale) { * * @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 @@ static final Object convertTemporalToObject(JDBCType jdbcType, SSType ssType, Ca 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 bb709307d..bccd66fe1 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -7229,7 +7229,7 @@ final Object readDateTime(int valueLength, Calendar appTimeZoneCalendar, JDBCTyp } // 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 +7246,7 @@ final Object readDate(int valueLength, Calendar appTimeZoneCalendar, JDBCType jd 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 +7262,7 @@ final Object readTime(int valueLength, TypeInfo typeInfo, Calendar appTimeZoneCa 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 +7276,7 @@ final Object readDateTime2(int valueLength, TypeInfo typeInfo, Calendar appTimeZ 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 +7291,7 @@ final Object readDateTimeOffset(int valueLength, TypeInfo typeInfo, JDBCType jdb 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 88fa73a38..8025410c9 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 @@ CallableStatement prepareCall(String sql, int nType, int nConcur, int nHold, */ 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/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 66ad92b9d..acdf6723c 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 void setDelayLoadingLobs(boolean b) { 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 @@ else if (0 == requestedPacketSize) } 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 T unwrap(Class iface) throws SQLException { /** original delayLoadingLobs */ private boolean originalDelayLoadingLobs; + + /** original ignoreOffsetOnDateTimeOffsetConversion */ + private boolean originalIgnoreOffsetOnDateTimeOffsetConversion; /** Always Encrypted version */ private int aeVersion = TDS.COLUMNENCRYPTION_NOT_SUPPORTED; @@ -7313,6 +7337,7 @@ void beginRequestInternal() throws SQLException { openStatements = new LinkedList<>(); originalUseFmtOnly = useFmtOnly; originalDelayLoadingLobs = delayLoadingLobs; + originalIgnoreOffsetOnDateTimeOffsetConversion = ignoreOffsetOnDateTimeOffsetConversion; requestStarted = true; } } finally { @@ -7371,6 +7396,9 @@ void endRequestInternal() throws SQLException { 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 168644353..248957b3a 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 @@ public void setDelayLoadingLobs(boolean delayLoadingLobs) { 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 fc4b1f73a..d5f401115 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/dtv.java b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java index 7fd3ba441..ab76347b4 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 @@ Object denormalizedValue(byte[] decryptedValue, JDBCType jdbcType, TypeInfo base // 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 @@ Object denormalizedValue(byte[] decryptedValue, JDBCType jdbcType, TypeInfo base 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 @@ Object denormalizedValue(byte[] decryptedValue, JDBCType jdbcType, TypeInfo base // 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 @@ Object denormalizedValue(byte[] decryptedValue, JDBCType jdbcType, TypeInfo base // 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 @@ Object denormalizedValue(byte[] decryptedValue, JDBCType jdbcType, TypeInfo base 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()); 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 d0ce25564..da2d4d73f 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 void testModifiableConnectionProperties() throws SQLException { 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 void testModifiableConnectionProperties() throws SQLException { 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 void testModifiableConnectionProperties() throws SQLException { 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 @@ private void setConnectionFields(SQLServerConnection con, boolean autoCommitMode 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 @@ private void setConnectionFields(SQLServerConnection con, boolean autoCommitMode 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 @@ private void compareValuesAgainstConnection(SQLServerConnection con, boolean aut "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 @@ private List getVerifiedMethodNames() { 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 5c6a5acc2..de8e160a2 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.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 void testNullValuesWithGetObject() throws Exception { } } + @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 *