From e47fdeb17b64059a17b826d47c126b736d07d196 Mon Sep 17 00:00:00 2001 From: Bobby Wertman Date: Mon, 27 Jul 2020 23:31:54 -0400 Subject: [PATCH 1/8] Support LocalDateTime in CallableStatement This PR adds support for the requested functionality in #1392, but does not address the mentioned code duplication. The added library code was copied verbatim from SQLServerResultSet. --- .../jdbc/SQLServerCallableStatement.java | 23 +++++++ .../CallableStatementTest.java | 60 +++++++++++++++++-- 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java index d864f2fa6..84324bda4 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java @@ -24,6 +24,7 @@ import java.sql.Time; import java.sql.Timestamp; import java.text.MessageFormat; +import java.time.LocalDateTime; import java.util.Calendar; import java.util.HashMap; import java.util.TreeMap; @@ -740,6 +741,20 @@ public T getObject(int index, Class type) throws SQLException { returnValue = getTime(index); } else if (type == java.sql.Timestamp.class) { returnValue = getTimestamp(index); + } else if (type == java.time.LocalDateTime.class || type == java.time.LocalDate.class + || type == java.time.LocalTime.class) { + java.time.LocalDateTime ldt = getLocalDateTime(index); + if (null == ldt) { + returnValue = null; + } else { + if (type == java.time.LocalDateTime.class) { + returnValue = ldt; + } else if (type == java.time.LocalDate.class) { + returnValue = ldt.toLocalDate(); + } else { + returnValue = ldt.toLocalTime(); + } + } } else if (type == microsoft.sql.DateTimeOffset.class) { returnValue = getDateTimeOffset(index); } else if (type == UUID.class) { @@ -893,6 +908,14 @@ public Timestamp getTimestamp(String name, Calendar cal) throws SQLServerExcepti return value; } + LocalDateTime getLocalDateTime(int columnIndex) throws SQLServerException { + loggerExternal.entering(getClassNameLogging(), "getLocalDateTime", columnIndex); + checkClosed(); + LocalDateTime value = (LocalDateTime) getValue(columnIndex, JDBCType.LOCALDATETIME); + loggerExternal.exiting(getClassNameLogging(), "getLocalDateTime", value); + return value; + } + @Override public Timestamp getDateTime(int index) throws SQLServerException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java index 139b8670a..4ae95cc51 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java @@ -3,13 +3,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; -import java.sql.CallableStatement; -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.sql.Types; +import java.sql.*; import java.text.MessageFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.TimeZone; import java.util.UUID; import org.junit.jupiter.api.AfterAll; @@ -39,6 +38,7 @@ public class CallableStatementTest extends AbstractTest { private static String outputProcedureNameGUID = RandomUtil.getIdentifier("uniqueidentifier_SP"); private static String setNullProcedureName = RandomUtil.getIdentifier("CallableStatementTest_setNull_SP"); private static String inputParamsProcedureName = RandomUtil.getIdentifier("CallableStatementTest_inputParams_SP"); + private static String getObjectLocalDateTimeProcedureName = RandomUtil.getIdentifier("CallableStatementTest_getObjectLocalDateTime_SP"); /** * Setup before test @@ -53,11 +53,13 @@ public static void setupTest() throws SQLException { TestUtils.dropProcedureIfExists(AbstractSQLGenerator.escapeIdentifier(outputProcedureNameGUID), stmt); TestUtils.dropProcedureIfExists(AbstractSQLGenerator.escapeIdentifier(setNullProcedureName), stmt); TestUtils.dropProcedureIfExists(AbstractSQLGenerator.escapeIdentifier(inputParamsProcedureName), stmt); + TestUtils.dropProcedureIfExists(AbstractSQLGenerator.escapeIdentifier(getObjectLocalDateTimeProcedureName), stmt); createGUIDTable(stmt); createGUIDStoredProcedure(stmt); createSetNullProcedure(stmt); createInputParamsProcedure(stmt); + createGetObjectLocalDateTimeProcedure(stmt); } } @@ -123,6 +125,45 @@ public void getSetNullWithTypeVarchar() throws SQLException { } } + + /** + * Tests getObject(n, java.time.LocalDateTime.class). + * + * @throws SQLException + */ + @Test + public void getObjectAsLocalDateTime() throws SQLException { + String sql = "{CALL " + AbstractSQLGenerator.escapeIdentifier(getObjectLocalDateTimeProcedureName) + " (?)}"; + try (Connection con = DriverManager.getConnection(connectionString); CallableStatement cs = con.prepareCall(sql)) { + cs.registerOutParameter(1, Types.TIMESTAMP); + TimeZone prevTimeZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("America/Edmonton")); + + // a local date/time that does not actually exist because of Daylight Saving Time + final String testValueDate = "2018-03-11"; + final String testValueTime = "02:00:00.1234567"; + final String testValueDateTime = testValueDate + "T" + testValueTime; + + try { + cs.execute(); + + LocalDateTime expectedLocalDateTime = LocalDateTime.parse(testValueDateTime); + LocalDateTime actualLocalDateTime = cs.getObject(1, LocalDateTime.class); + assertEquals(expectedLocalDateTime, actualLocalDateTime); + + LocalDate expectedLocalDate = LocalDate.parse(testValueDate); + LocalDate actualLocalDate = cs.getObject(1, LocalDate.class); + assertEquals(expectedLocalDate, actualLocalDate); + + LocalTime expectedLocalTime = LocalTime.parse(testValueTime); + LocalTime actualLocalTime = cs.getObject(1, LocalTime.class); + assertEquals(expectedLocalTime, actualLocalTime); + } finally { + TimeZone.setDefault(prevTimeZone); + } + } + } + /** * recognize parameter names with and without leading '@' * @@ -207,4 +248,11 @@ private static void createInputParamsProcedure(Statement stmt) throws SQLExcepti stmt.execute(sql); } + + private static void createGetObjectLocalDateTimeProcedure(Statement stmt) throws SQLException { + String sql = "CREATE PROCEDURE " + AbstractSQLGenerator.escapeIdentifier(getObjectLocalDateTimeProcedureName) + + "(@p1 datetime2(7) OUTPUT) AS " + + "SELECT @p1 = '2018-03-11T02:00:00.1234567'"; + stmt.execute(sql); + } } From 0e33a289512af1904189575a49491f9640f2227c Mon Sep 17 00:00:00 2001 From: Bobby Wertman Date: Wed, 26 Aug 2020 21:58:36 -0400 Subject: [PATCH 2/8] Fix import style --- .../jdbc/callablestatement/CallableStatementTest.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java index 4ae95cc51..05e833b33 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java @@ -3,7 +3,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; -import java.sql.*; +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Types; import java.text.MessageFormat; import java.time.LocalDate; import java.time.LocalDateTime; From e5ccbb74b10d1d3e8655cd670ac6de1b645b9ba9 Mon Sep 17 00:00:00 2001 From: Bobby Wertman Date: Wed, 26 Aug 2020 22:01:21 -0400 Subject: [PATCH 3/8] Drop procedure after tests --- .../sqlserver/jdbc/callablestatement/CallableStatementTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java index 05e833b33..e8cd54fe3 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java @@ -226,6 +226,7 @@ public static void cleanup() throws SQLException { TestUtils.dropProcedureIfExists(AbstractSQLGenerator.escapeIdentifier(outputProcedureNameGUID), stmt); TestUtils.dropProcedureIfExists(AbstractSQLGenerator.escapeIdentifier(setNullProcedureName), stmt); TestUtils.dropProcedureIfExists(AbstractSQLGenerator.escapeIdentifier(inputParamsProcedureName), stmt); + TestUtils.dropProcedureIfExists(AbstractSQLGenerator.escapeIdentifier(getObjectLocalDateTimeProcedureName), stmt); } } From f3c78bb8d358d301346010e5397a200e77c23e8f Mon Sep 17 00:00:00 2001 From: Bobby Wertman Date: Wed, 26 Aug 2020 22:02:08 -0400 Subject: [PATCH 4/8] Rename test method --- .../sqlserver/jdbc/callablestatement/CallableStatementTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java index e8cd54fe3..0aaa9061a 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java @@ -138,7 +138,7 @@ public void getSetNullWithTypeVarchar() throws SQLException { * @throws SQLException */ @Test - public void getObjectAsLocalDateTime() throws SQLException { + public void testGetObjectAsLocalDateTime() throws SQLException { String sql = "{CALL " + AbstractSQLGenerator.escapeIdentifier(getObjectLocalDateTimeProcedureName) + " (?)}"; try (Connection con = DriverManager.getConnection(connectionString); CallableStatement cs = con.prepareCall(sql)) { cs.registerOutParameter(1, Types.TIMESTAMP); From 9918292d80790d0189cf4d54a2d7da0e4b0ac4fa Mon Sep 17 00:00:00 2001 From: Bobby Wertman Date: Wed, 26 Aug 2020 22:03:33 -0400 Subject: [PATCH 5/8] Copy OffsetDateTime support from ResultSet --- .../jdbc/SQLServerCallableStatement.java | 14 ++++++ .../CallableStatementTest.java | 43 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java index 84324bda4..7f5097f0f 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java @@ -755,6 +755,20 @@ public T getObject(int index, Class type) throws SQLException { returnValue = ldt.toLocalTime(); } } + } else if (type == java.time.OffsetDateTime.class) { + microsoft.sql.DateTimeOffset dateTimeOffset = getDateTimeOffset(index); + if (dateTimeOffset == null) { + returnValue = null; + } else { + returnValue = dateTimeOffset.getOffsetDateTime(); + } + } else if (type == java.time.OffsetTime.class) { + microsoft.sql.DateTimeOffset dateTimeOffset = getDateTimeOffset(index); + if (dateTimeOffset == null) { + returnValue = null; + } else { + returnValue = dateTimeOffset.getOffsetDateTime().toOffsetTime(); + } } else if (type == microsoft.sql.DateTimeOffset.class) { returnValue = getDateTimeOffset(index); } else if (type == UUID.class) { diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java index 0aaa9061a..b2d2b9813 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java @@ -1,6 +1,7 @@ package com.microsoft.sqlserver.jdbc.callablestatement; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.fail; import java.sql.CallableStatement; @@ -11,6 +12,8 @@ import java.sql.Statement; import java.sql.Types; import java.text.MessageFormat; +import java.time.OffsetDateTime; +import java.time.OffsetTime; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; @@ -45,6 +48,7 @@ public class CallableStatementTest extends AbstractTest { private static String setNullProcedureName = RandomUtil.getIdentifier("CallableStatementTest_setNull_SP"); private static String inputParamsProcedureName = RandomUtil.getIdentifier("CallableStatementTest_inputParams_SP"); private static String getObjectLocalDateTimeProcedureName = RandomUtil.getIdentifier("CallableStatementTest_getObjectLocalDateTime_SP"); + private static String getObjectOffsetDateTimeProcedureName = RandomUtil.getIdentifier("CallableStatementTest_getObjectOffsetDateTime_SP"); /** * Setup before test @@ -60,12 +64,14 @@ public static void setupTest() throws SQLException { TestUtils.dropProcedureIfExists(AbstractSQLGenerator.escapeIdentifier(setNullProcedureName), stmt); TestUtils.dropProcedureIfExists(AbstractSQLGenerator.escapeIdentifier(inputParamsProcedureName), stmt); TestUtils.dropProcedureIfExists(AbstractSQLGenerator.escapeIdentifier(getObjectLocalDateTimeProcedureName), stmt); + TestUtils.dropProcedureIfExists(AbstractSQLGenerator.escapeIdentifier(getObjectOffsetDateTimeProcedureName), stmt); createGUIDTable(stmt); createGUIDStoredProcedure(stmt); createSetNullProcedure(stmt); createInputParamsProcedure(stmt); createGetObjectLocalDateTimeProcedure(stmt); + createGetObjectOffsetDateTimeProcedure(stmt); } } @@ -170,6 +176,35 @@ public void testGetObjectAsLocalDateTime() throws SQLException { } } + /** + * Tests getObject(n, java.time.OffsetDateTime.class) and getObject(n, java.time.OffsetTime.class). + * + * @throws SQLException + */ + @Test + @Tag(Constants.xAzureSQLDW) + public void testGetObjectAsOffsetDateTime() throws SQLException { + String sql = "{CALL " + AbstractSQLGenerator.escapeIdentifier(getObjectOffsetDateTimeProcedureName) + " (?)}"; + try (Connection con = DriverManager.getConnection(connectionString); CallableStatement cs = con.prepareCall(sql)) { + cs.registerOutParameter(1, Types.TIMESTAMP); + cs.registerOutParameter(2, Types.TIMESTAMP); + + final String testValue = "2018-01-02T11:22:33.123456700+12:34"; + + cs.execute(); + + OffsetDateTime expected = OffsetDateTime.parse(testValue); + OffsetDateTime actual = cs.getObject(1, OffsetDateTime.class); + assertEquals(expected, actual); + assertNull(cs.getObject(2, OffsetDateTime.class)); + + OffsetTime expectedTime = OffsetTime.parse(testValue.split("T")[1]); + OffsetTime actualTime = cs.getObject(1, OffsetTime.class); + assertEquals(expectedTime, actualTime); + assertNull(cs.getObject(2, OffsetTime.class)); + } + } + /** * recognize parameter names with and without leading '@' * @@ -227,6 +262,7 @@ public static void cleanup() throws SQLException { TestUtils.dropProcedureIfExists(AbstractSQLGenerator.escapeIdentifier(setNullProcedureName), stmt); TestUtils.dropProcedureIfExists(AbstractSQLGenerator.escapeIdentifier(inputParamsProcedureName), stmt); TestUtils.dropProcedureIfExists(AbstractSQLGenerator.escapeIdentifier(getObjectLocalDateTimeProcedureName), stmt); + TestUtils.dropProcedureIfExists(AbstractSQLGenerator.escapeIdentifier(getObjectOffsetDateTimeProcedureName), stmt); } } @@ -262,4 +298,11 @@ private static void createGetObjectLocalDateTimeProcedure(Statement stmt) throws + "SELECT @p1 = '2018-03-11T02:00:00.1234567'"; stmt.execute(sql); } + + private static void createGetObjectOffsetDateTimeProcedure(Statement stmt) throws SQLException { + String sql = "CREATE PROCEDURE " + AbstractSQLGenerator.escapeIdentifier(getObjectOffsetDateTimeProcedureName) + + "(@p1 datetime2(7) DATETIMEOFFSET, @p2 datetime2(7) DATETIMEOFFSET) AS " + + "SELECT @p1 = '2018-01-02T11:22:33.123456700+12:34', @p2 = NULL"; + stmt.execute(sql); + } } From f57fa5abb79295e6418826d3049a63a1d41a7da9 Mon Sep 17 00:00:00 2001 From: Bobby Wertman Date: Wed, 26 Aug 2020 22:09:05 -0400 Subject: [PATCH 6/8] Fix output param syntax --- .../sqlserver/jdbc/callablestatement/CallableStatementTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java index b2d2b9813..c1586c9ff 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java @@ -301,7 +301,7 @@ private static void createGetObjectLocalDateTimeProcedure(Statement stmt) throws private static void createGetObjectOffsetDateTimeProcedure(Statement stmt) throws SQLException { String sql = "CREATE PROCEDURE " + AbstractSQLGenerator.escapeIdentifier(getObjectOffsetDateTimeProcedureName) - + "(@p1 datetime2(7) DATETIMEOFFSET, @p2 datetime2(7) DATETIMEOFFSET) AS " + + "(@p1 DATETIMEOFFSET OUTPUT, @p2 DATETIMEOFFSET OUTPUT) AS " + "SELECT @p1 = '2018-01-02T11:22:33.123456700+12:34', @p2 = NULL"; stmt.execute(sql); } From 8d1b628681bbb8c66eb0f118cf33fcf07c9b460e Mon Sep 17 00:00:00 2001 From: Bobby Wertman Date: Wed, 26 Aug 2020 22:35:54 -0400 Subject: [PATCH 7/8] Include correct number of parameters in call --- .../sqlserver/jdbc/callablestatement/CallableStatementTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java index c1586c9ff..f75c9e00b 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java @@ -184,7 +184,7 @@ public void testGetObjectAsLocalDateTime() throws SQLException { @Test @Tag(Constants.xAzureSQLDW) public void testGetObjectAsOffsetDateTime() throws SQLException { - String sql = "{CALL " + AbstractSQLGenerator.escapeIdentifier(getObjectOffsetDateTimeProcedureName) + " (?)}"; + String sql = "{CALL " + AbstractSQLGenerator.escapeIdentifier(getObjectOffsetDateTimeProcedureName) + " (?, ?)}"; try (Connection con = DriverManager.getConnection(connectionString); CallableStatement cs = con.prepareCall(sql)) { cs.registerOutParameter(1, Types.TIMESTAMP); cs.registerOutParameter(2, Types.TIMESTAMP); From dcb2b484fa8a52d2b2d5999b3ee00c2793cfd2e6 Mon Sep 17 00:00:00 2001 From: Bobby Wertman Date: Wed, 26 Aug 2020 22:42:18 -0400 Subject: [PATCH 8/8] Use correct output type --- .../jdbc/callablestatement/CallableStatementTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java index f75c9e00b..68b9bb06c 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java @@ -186,8 +186,8 @@ public void testGetObjectAsLocalDateTime() throws SQLException { public void testGetObjectAsOffsetDateTime() throws SQLException { String sql = "{CALL " + AbstractSQLGenerator.escapeIdentifier(getObjectOffsetDateTimeProcedureName) + " (?, ?)}"; try (Connection con = DriverManager.getConnection(connectionString); CallableStatement cs = con.prepareCall(sql)) { - cs.registerOutParameter(1, Types.TIMESTAMP); - cs.registerOutParameter(2, Types.TIMESTAMP); + cs.registerOutParameter(1, Types.TIMESTAMP_WITH_TIMEZONE); + cs.registerOutParameter(2, Types.TIMESTAMP_WITH_TIMEZONE); final String testValue = "2018-01-02T11:22:33.123456700+12:34";