From 2e424a41457d1f788cd9e9334c9ade9d9fb4990b Mon Sep 17 00:00:00 2001 From: lilgreenbird Date: Wed, 10 Jan 2024 16:24:20 -0800 Subject: [PATCH 01/47] fixed table name (#2292) --- .../jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java index 4f5a4d66b..ec40af443 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java @@ -819,7 +819,7 @@ public void testNoSpaceInsert() throws Exception { f1.setAccessible(true); f1.set(connection, true); - TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableNameBulkComputedCols), stmt); + TestUtils.dropTableIfExists(testNoSpaceInsertTableName, stmt); String createTable = "create table " + testNoSpaceInsertTableName + " (id nvarchar(100) not null, json nvarchar(max) not null," + " vcol1 as json_value([json], '$.vcol1'), vcol2 as json_value([json], '$.vcol2'))"; From 8289086df554bf8b9e60f59b7a6a7fbd043f9cdd Mon Sep 17 00:00:00 2001 From: Jeffery Wasty Date: Thu, 11 Jan 2024 16:03:08 -0800 Subject: [PATCH 02/47] Update README.md for release version clarification. (#2296) --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ffbc04353..6befca937 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ Welcome to the Microsoft JDBC Driver for SQL Server project! The Microsoft JDBC Driver for SQL Server is a Type 4 JDBC driver that provides database connectivity through the standard JDBC application program interfaces (APIs) available in the Java Platform, Enterprise Editions. The Driver provides access to Microsoft SQL Server and Azure SQL Database from any Java application, application server, or Java-enabled applet. +Releases can be found on the [GitHub Releases](https://github.com/microsoft/mssql-jdbc/releases) page, in the [Microsoft JDBC Documentation](https://learn.microsoft.com/en-us/sql/connect/jdbc/download-microsoft-jdbc-driver-for-sql-server?view=sql-server-ver16), or via Maven. Starting from preview release 12.1.0, each release contains two versions of the driver. One for use with Java 8 (jre8), and one for use with version Java 11 and above (jre11). + We hope you enjoy using the Microsoft JDBC Driver for SQL Server. Microsoft JDBC driver for SQL Server Team @@ -82,7 +84,7 @@ We're now on the Maven Central Repository. Add the following to your POM file to 12.4.1.jre11 ``` -The driver can be downloaded from [Microsoft](https://aka.ms/downloadmssqljdbc). +The driver can be downloaded from [Microsoft](https://aka.ms/downloadmssqljdbc). For driver version 12.1.0 and greater, please use the jre11 version when using Java 11 or greater, and the jre8 version when using Java 8. To get the latest version of the driver, add the following to your POM file: From e9805902138c2ed2ae44814185c53a52490ee466 Mon Sep 17 00:00:00 2001 From: lilgreenbird Date: Fri, 12 Jan 2024 17:04:15 -0800 Subject: [PATCH 03/47] Fixed typos in #2274 and also some minor code cleanup and test fixes (#2297) --- ...verColumnEncryptionJavaKeyStoreProvider.java | 17 ----------------- .../sqlserver/jdbc/SQLServerConnection.java | 9 ++++++--- .../sqlserver/jdbc/connection/TimeoutTest.java | 7 ++++--- .../BatchExecutionWithBulkCopyTest.java | 6 ++++-- 4 files changed, 14 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java index 319b2b268..50359f04a 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java @@ -391,21 +391,4 @@ private byte[] getLittleEndianBytesFromShort(short value) { byteBuffer.order(ByteOrder.LITTLE_ENDIAN); return byteBuffer.putShort(value).array(); } - - /* - * Verify signature against certificate - */ - private boolean rsaVerifySignature(byte[] dataToVerify, byte[] signature, - CertificateDetails certificateDetails) throws InvalidKeyException, NoSuchAlgorithmException, SignatureException { - Signature sig = Signature.getInstance("SHA256withRSA"); - - sig.initSign((PrivateKey) certificateDetails.privateKey); - sig.update(dataToVerify); - - byte[] signedHash = sig.sign(); - - sig.initVerify(certificateDetails.certificate.getPublicKey()); - sig.update(dataToVerify); - return sig.verify(signature); - } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 93cc2dec1..c4563e091 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -315,6 +315,9 @@ public String toString() { /** * Generate a 6 byte random array for netAddress + * As per TDS spec this is a unique clientID (MAC address) used to identify the client. + * A random number is used instead of the actual MAC address to avoid PII issues. + * As per spec this is informational only server does not process this so there is no need to use SecureRandom. * * @return byte[] */ @@ -2137,9 +2140,9 @@ void validateConnectionRetry() throws SQLServerException { // Set to larger default value for Azure connections to greatly improve recovery if (isAzureSynapseOnDemandEndpoint()) { - connectRetryCount = AZURE_SERVER_ENDPOINT_RETRY_COUNT_DEFAULT; - } else if (isAzureSqlServerEndpoint()) { connectRetryCount = AZURE_SYNAPSE_ONDEMAND_ENDPOINT_RETRY_COUNT_DEFAFULT; + } else if (isAzureSqlServerEndpoint()) { + connectRetryCount = AZURE_SERVER_ENDPOINT_RETRY_COUNT_DEFAULT; } } } @@ -8382,7 +8385,7 @@ boolean isAzureSynapseOnDemandEndpoint() { int px = serverName.indexOf('\\'); String parsedServerName = (px >= 0) ? serverName.substring(0, px) : serverName; - return AzureSQLServerEndpoints.isAzureSqlServerEndpoint(parsedServerName); + return AzureSQLServerEndpoints.isAzureSynapseOnDemandEndpoint(parsedServerName); } return false; 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 2a0e6b04b..f7c44be06 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java @@ -269,10 +269,11 @@ public void testAzureEndpointRetry() { if (f.getName() == "connectRetryCount") { f.setAccessible(true); int retryCount = f.getInt(con); - if (TestUtils.isAzure(con)) { - assertTrue(retryCount == 2); // AZURE_SERVER_ENDPOINT_RETRY_COUNT_DEFAFULT - } else if (TestUtils.isAzureDW(con)) { + + if (TestUtils.isAzureDW(con)) { assertTrue(retryCount == 5); // AZURE_SYNAPSE_ONDEMAND_ENDPOINT_RETRY_COUNT_DEFAFULT + } else if (TestUtils.isAzure(con)) { + assertTrue(retryCount == 2); // AZURE_SERVER_ENDPOINT_RETRY_COUNT_DEFAFULT } else { assertTrue(retryCount == 1); // default connectRetryCount } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java index ec40af443..00ff25b67 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java @@ -819,8 +819,8 @@ public void testNoSpaceInsert() throws Exception { f1.setAccessible(true); f1.set(connection, true); - TestUtils.dropTableIfExists(testNoSpaceInsertTableName, stmt); - String createTable = "create table " + testNoSpaceInsertTableName + TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(testNoSpaceInsertTableName), stmt); + String createTable = "create table " + AbstractSQLGenerator.escapeIdentifier(testNoSpaceInsertTableName) + " (id nvarchar(100) not null, json nvarchar(max) not null," + " vcol1 as json_value([json], '$.vcol1'), vcol2 as json_value([json], '$.vcol2'))"; stmt.execute(createTable); @@ -832,6 +832,8 @@ public void testNoSpaceInsert() throws Exception { pstmt.setString(2, jsonValue); pstmt.addBatch(); pstmt.executeBatch(); + } catch (Exception e) { + fail(testNoSpaceInsertTableName + ": " + e.getMessage()); } finally { try (Statement stmt = connection.createStatement()) { TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(testNoSpaceInsertTableName), stmt); From a6004e9c4521fccf8f4ebec5d7995ec44f6b383c Mon Sep 17 00:00:00 2001 From: lilgreenbird Date: Mon, 15 Jan 2024 10:55:02 -0800 Subject: [PATCH 04/47] fix test (#2298) --- .../BatchExecutionWithBulkCopyTest.java | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java index 00ff25b67..4d263d007 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; +import java.util.Random; import java.util.UUID; import org.junit.jupiter.api.AfterAll; @@ -809,31 +810,20 @@ public void testComputedCols() throws Exception { @Test public void testNoSpaceInsert() throws Exception { // table name with valid alphanumeric chars that don't need to be escaped, since escaping the table name would not test the space issue - String testNoSpaceInsertTableName = "testNoSpaceInsertTable" + RandomData.generateInt(false); - String valid = "insert into " + testNoSpaceInsertTableName + "(id, json)" + " values(?, ?)"; + String testNoSpaceInsertTableName = "testNoSpaceInsertTable" + (new Random()).nextInt(Integer.MAX_VALUE); + String valid = "insert into " + testNoSpaceInsertTableName + "(col)" + " values(?)"; try (Connection connection = PrepUtil.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); Statement stmt = (SQLServerStatement) connection.createStatement();) { - Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); - f1.setAccessible(true); - f1.set(connection, true); - TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(testNoSpaceInsertTableName), stmt); - String createTable = "create table " + AbstractSQLGenerator.escapeIdentifier(testNoSpaceInsertTableName) - + " (id nvarchar(100) not null, json nvarchar(max) not null," - + " vcol1 as json_value([json], '$.vcol1'), vcol2 as json_value([json], '$.vcol2'))"; + TestUtils.dropTableIfExists(testNoSpaceInsertTableName, stmt); + String createTable = "create table " + testNoSpaceInsertTableName + " (col varchar(4))"; stmt.execute(createTable); - String jsonValue = "{\"vcol1\":\"" + UUID.randomUUID().toString() + "\",\"vcol2\":\"" - + UUID.randomUUID().toString() + "\" }"; - String idValue = UUID.randomUUID().toString(); - pstmt.setString(1, idValue); - pstmt.setString(2, jsonValue); + pstmt.setString(1, "test"); pstmt.addBatch(); pstmt.executeBatch(); - } catch (Exception e) { - fail(testNoSpaceInsertTableName + ": " + e.getMessage()); } finally { try (Statement stmt = connection.createStatement()) { TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(testNoSpaceInsertTableName), stmt); From 0f04ce679d83a04cfc84eed4b497ff64604ec094 Mon Sep 17 00:00:00 2001 From: jesperah <156679721+jesperah@users.noreply.github.com> Date: Mon, 15 Jan 2024 22:08:19 +0100 Subject: [PATCH 05/47] missing null check of e.getCause(). (#2300) if the caught exception does not have a cause then pass the caught exception as the cause to the SQLTimeoutException instead of the null "e.getCause()". (#2299) Co-authored-by: jesperah --- .../com/microsoft/sqlserver/jdbc/SQLServerStatement.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java index 4e096864d..866b9ea13 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java @@ -262,10 +262,14 @@ final void executeStatement(TDSCommand newStmtCmd) throws SQLServerException, SQ // (Re)execute this Statement with the new command executeCommand(newStmtCmd); } catch (SQLServerException e) { - if (e.getDriverErrorCode() == SQLServerException.ERROR_QUERY_TIMEOUT) + if (e.getDriverErrorCode() == SQLServerException.ERROR_QUERY_TIMEOUT) { + if (e.getCause() == null) { + throw new SQLTimeoutException(e.getMessage(), e.getSQLState(), e.getErrorCode(), e); + } throw new SQLTimeoutException(e.getMessage(), e.getSQLState(), e.getErrorCode(), e.getCause()); - else + } else { throw e; + } } finally { if (newStmtCmd.wasExecuted()) lastStmtExecCmd = newStmtCmd; From 066aeeb9a992394bf4a7d5ae2e406c8d0587e034 Mon Sep 17 00:00:00 2001 From: lilgreenbird Date: Mon, 15 Jan 2024 15:15:13 -0800 Subject: [PATCH 06/47] Update test for Synapse OnDemand servers (#2302) --- .../java/com/microsoft/sqlserver/jdbc/TestUtils.java | 9 +++++++++ .../microsoft/sqlserver/jdbc/connection/TimeoutTest.java | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java index 2f20821a1..cd20087c1 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java @@ -237,6 +237,15 @@ public static boolean isAzureMI(Connection con) { return ((SQLServerConnection) con).isAzureMI(); } + /** + * Checks if connection is established to Azure Synapse OnDemand server + * + */ + public static boolean isAzureSynapseOnDemand(Connection con) { + isAzure(con); + return ((SQLServerConnection) con).isAzureSynapseOnDemandEndpoint(); + } + /** * Checks if connection is established to server that supports AEv2. * 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 f7c44be06..49b05e713 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java @@ -266,12 +266,12 @@ public void testAzureEndpointRetry() { try (Connection con = PrepUtil.getConnection(connectionString)) { Field fields[] = con.getClass().getSuperclass().getDeclaredFields(); for (Field f : fields) { - if (f.getName() == "connectRetryCount") { + if (f.getName().equals("connectRetryCount")) { f.setAccessible(true); int retryCount = f.getInt(con); - if (TestUtils.isAzureDW(con)) { - assertTrue(retryCount == 5); // AZURE_SYNAPSE_ONDEMAND_ENDPOINT_RETRY_COUNT_DEFAFULT + if (TestUtils.isAzureSynapseOnDemand(con)) { + assertTrue(retryCount == 5); // AZURE_SYNAPSE_ONDEMAND_ENDPOINT_RETRY_COUNT_DEFAULT } else if (TestUtils.isAzure(con)) { assertTrue(retryCount == 2); // AZURE_SERVER_ENDPOINT_RETRY_COUNT_DEFAFULT } else { From 5f71d607d4203f76dba79dc640890279fc750c55 Mon Sep 17 00:00:00 2001 From: Jeffery Wasty Date: Wed, 17 Jan 2024 09:30:40 -0800 Subject: [PATCH 07/47] Fix lock issues around disableSSL and IOBuffer (#2295) * Renamed locks in IOBuffer, disableSSL should have had its own lock * Renamed locks in IOBuffer, disableSSL should have had its own lock * TDSChannelLock --> tdsChannelLock * Renamed TDSReader lock --> tdsReaderLock * TDSChannelLock --> tdsChannelLock * Renamed TDSReader lock --> tdsReaderLock --- .../microsoft/sqlserver/jdbc/IOBuffer.java | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index 14c7b0d4a..b70d66bfb 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -672,7 +672,7 @@ final boolean isLoggingPackets() { int numMsgsSent = 0; int numMsgsRcvd = 0; - private final transient Lock lock = new ReentrantLock(); + private final transient Lock tdsChannelLock = new ReentrantLock(); // Last SPID received from the server. Used for logging and to tag subsequent outgoing // packets to facilitate diagnosing problems from the server side. @@ -773,7 +773,7 @@ void disableSSL() { logger.finer(toString() + " Disabling SSL..."); } - lock.lock(); + tdsChannelLock.lock(); try { // Guard in case of disableSSL being called before enableSSL if (proxySocket == null) { @@ -839,7 +839,7 @@ void disableSSL() { channelSocket = tcpSocket; sslSocket = null; } finally { - lock.unlock(); + tdsChannelLock.unlock(); } if (logger.isLoggable(Level.FINER)) @@ -1056,6 +1056,8 @@ private void writeInternal(byte[] b, int off, int len) throws IOException { private final class ProxyInputStream extends InputStream { private InputStream filteredStream; + private final transient Lock proxyInputStreamLock = new ReentrantLock(); + /** * Bytes that have been read by a poll(s). */ @@ -1082,7 +1084,7 @@ final void setFilteredStream(InputStream is) { * If an I/O exception occurs. */ public boolean poll() { - lock.lock(); + proxyInputStreamLock.lock(); try { int b; try { @@ -1117,7 +1119,7 @@ public boolean poll() { return true; } finally { - lock.unlock(); + proxyInputStreamLock.unlock(); } } @@ -1133,7 +1135,7 @@ private int getOneFromCache() { @Override public long skip(long n) throws IOException { - lock.lock(); + proxyInputStreamLock.lock(); try { long bytesSkipped = 0; @@ -1154,7 +1156,7 @@ public long skip(long n) throws IOException { return bytesSkipped; } finally { - lock.unlock(); + proxyInputStreamLock.unlock(); } } @@ -1191,7 +1193,7 @@ public int read(byte[] b, int offset, int maxBytes) throws IOException { } private int readInternal(byte[] b, int offset, int maxBytes) throws IOException { - lock.lock(); + proxyInputStreamLock.lock(); try { int bytesRead; @@ -1240,7 +1242,7 @@ private int readInternal(byte[] b, int offset, int maxBytes) throws IOException return bytesRead; } finally { - lock.unlock(); + proxyInputStreamLock.unlock(); } } @@ -1259,11 +1261,11 @@ public void mark(int readLimit) { if (logger.isLoggable(Level.FINEST)) logger.finest(super.toString() + " Marking next " + readLimit + " bytes"); - lock.lock(); + proxyInputStreamLock.lock(); try { filteredStream.mark(readLimit); } finally { - lock.unlock(); + proxyInputStreamLock.unlock(); } } @@ -1272,12 +1274,12 @@ public void reset() throws IOException { if (logger.isLoggable(Level.FINEST)) logger.finest(super.toString() + " Resetting to previous mark"); - lock.lock(); + proxyInputStreamLock.lock(); try { filteredStream.reset(); } finally { - lock.unlock(); + proxyInputStreamLock.unlock(); } } @@ -6688,7 +6690,7 @@ final SQLServerConnection getConnection() { private boolean serverSupportsColumnEncryption = false; private boolean serverSupportsDataClassification = false; private byte serverSupportedDataClassificationVersion = TDS.DATA_CLASSIFICATION_NOT_ENABLED; - private final transient Lock lock = new ReentrantLock(); + private final transient Lock tdsReaderLock = new ReentrantLock(); private final byte[] valueBytes = new byte[256]; @@ -6808,7 +6810,7 @@ private boolean nextPacket() throws SQLServerException { * the response and another thread that is trying to buffer it with TDSCommand.detach(). */ final boolean readPacket() throws SQLServerException { - lock.lock(); + tdsReaderLock.lock(); try { if (null != command && !command.readingResponse()) return false; @@ -6921,7 +6923,7 @@ final boolean readPacket() throws SQLServerException { return true; } finally { - lock.unlock(); + tdsReaderLock.unlock(); } } From 829321f23eecc47dcbcc380e238d5c65e9ac0c56 Mon Sep 17 00:00:00 2001 From: Jeffery Wasty Date: Wed, 17 Jan 2024 13:12:17 -0800 Subject: [PATCH 08/47] Added cleanup task to start of every execute. (#2272) * Initial commit. Add the cleanup task to any execute task to clean up on execute. * Remove added lines in test file * Remove added imports * Quick test fix try * Line should be removed, not just commented out. * Added missing execute cases * Discarded count should always be 1 * Mistake revert * Another try * Reverted test changes to try to get some consistency --- .../sqlserver/jdbc/SQLServerPreparedStatement.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index 79d9f1e27..bb8e49b9c 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -487,6 +487,7 @@ public java.sql.ResultSet executeQuery() throws SQLServerException, SQLTimeoutEx loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString()); } checkClosed(); + connection.unprepareUnreferencedPreparedStatementHandles(false); executeStatement(new PrepStmtExecCmd(this, EXECUTE_QUERY)); loggerExternal.exiting(getClassNameLogging(), "executeQuery"); return resultSet; @@ -501,6 +502,7 @@ public java.sql.ResultSet executeQuery() throws SQLServerException, SQLTimeoutEx */ final java.sql.ResultSet executeQueryInternal() throws SQLServerException, SQLTimeoutException { checkClosed(); + connection.unprepareUnreferencedPreparedStatementHandles(false); executeStatement(new PrepStmtExecCmd(this, EXECUTE_QUERY_INTERNAL)); return resultSet; } @@ -513,7 +515,7 @@ public int executeUpdate() throws SQLServerException, SQLTimeoutException { } checkClosed(); - + connection.unprepareUnreferencedPreparedStatementHandles(false); executeStatement(new PrepStmtExecCmd(this, EXECUTE_UPDATE)); // this shouldn't happen, caller probably meant to call executeLargeUpdate @@ -534,6 +536,7 @@ public long executeLargeUpdate() throws SQLServerException, SQLTimeoutException loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString()); } checkClosed(); + connection.unprepareUnreferencedPreparedStatementHandles(false); executeStatement(new PrepStmtExecCmd(this, EXECUTE_UPDATE)); loggerExternal.exiting(getClassNameLogging(), "executeLargeUpdate", updateCount); return updateCount; @@ -546,6 +549,7 @@ public boolean execute() throws SQLServerException, SQLTimeoutException { loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString()); } checkClosed(); + connection.unprepareUnreferencedPreparedStatementHandles(false); executeStatement(new PrepStmtExecCmd(this, EXECUTE)); loggerExternal.exiting(getClassNameLogging(), "execute", null != resultSet); return null != resultSet; @@ -2199,6 +2203,7 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString()); } checkClosed(); + connection.unprepareUnreferencedPreparedStatementHandles(false); discardLastExecutionResults(); try { @@ -2382,6 +2387,7 @@ public long[] executeLargeBatch() throws SQLServerException, BatchUpdateExceptio loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString()); } checkClosed(); + connection.unprepareUnreferencedPreparedStatementHandles(false); discardLastExecutionResults(); try { From bb76a7832ecf1cd24238c75cf7c2c4706d922435 Mon Sep 17 00:00:00 2001 From: Terry Chow <32403408+tkyc@users.noreply.github.com> Date: Fri, 19 Jan 2024 10:10:18 -0800 Subject: [PATCH 09/47] Apply timezone to Timestamp when using bulkcopy for batch insert (#2291) * Apply timezone to Timestamps for batch insert with bulkcopy * Formatted files * Removed test comment * Code review, adjusted implementation * Added additional test * New implementation; TODO: cleanup PR * New fix implementation * Removed unused imports * Code review * Bulkcopy CSV test fix --- .../microsoft/sqlserver/jdbc/IOBuffer.java | 30 ++- .../sqlserver/jdbc/ISQLServerConnection.java | 6 +- .../jdbc/SQLServerBulkBatchInsertRecord.java | 2 +- .../sqlserver/jdbc/SQLServerBulkCopy.java | 35 +++- .../jdbc/SQLServerCallableStatement.java | 3 +- .../jdbc/SQLServerConnectionPoolProxy.java | 5 +- .../sqlserver/jdbc/SQLServerDriver.java | 4 +- .../jdbc/SQLServerPreparedStatement.java | 2 + .../jdbc/SQLServerConnectionTest.java | 3 +- .../jdbc/resiliency/BasicConnectionTest.java | 2 +- .../unit/statement/BatchExecutionTest.java | 190 ++++++++++++++++++ 11 files changed, 262 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index b70d66bfb..e5e175b9e 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -1056,7 +1056,7 @@ private void writeInternal(byte[] b, int off, int len) throws IOException { private final class ProxyInputStream extends InputStream { private InputStream filteredStream; - private final transient Lock proxyInputStreamLock = new ReentrantLock(); + private final Lock proxyInputStreamLock = new ReentrantLock(); /** * Bytes that have been read by a poll(s). @@ -3825,6 +3825,20 @@ void writeDate(String value) throws SQLServerException { SSType.DATE); } + void writeDate(long utcMillis, Calendar cal) throws SQLServerException { + GregorianCalendar calendar = initializeCalender(TimeZone.getDefault()); + + // Load the calendar with the desired value + calendar.setTimeInMillis(utcMillis); + if (cal != null) { + calendar.setTimeZone(cal.getTimeZone()); + } + + writeScaledTemporal(calendar, 0, // subsecond nanos (none for a date value) + 0, // scale (dates are not scaled) + SSType.DATE); + } + void writeTime(java.sql.Timestamp value, int scale) 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) @@ -3838,6 +3852,20 @@ void writeTime(java.sql.Timestamp value, int scale) throws SQLServerException { writeScaledTemporal(calendar, subSecondNanos, scale, SSType.TIME); } + void writeTime(java.sql.Timestamp value, int scale, Calendar cal) throws SQLServerException { + GregorianCalendar calendar = initializeCalender(TimeZone.getDefault()); + long utcMillis = value.getTime(); // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT) + int subSecondNanos = value.getNanos(); + + // Load the calendar with the desired value + calendar.setTimeInMillis(utcMillis); + if (cal != null) { + calendar.setTimeZone(cal.getTimeZone()); + } + + writeScaledTemporal(calendar, subSecondNanos, scale, SSType.TIME); + } + void writeDateTimeOffset(Object value, int scale, SSType destSSType) throws SQLServerException { GregorianCalendar calendar; TimeZone timeZone; // Time zone to associate with the value in the Gregorian calendar diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java index 7ab7772d7..c49b553ae 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java @@ -498,14 +498,14 @@ CallableStatement prepareCall(String sql, int nType, int nConcur, int nHold, */ void setCalcBigDecimalPrecision(boolean calcBigDecimalPrecision); - /** + /** * Specifies the flag for using Bulk Copy API for batch insert operations. * * @param useBulkCopyForBatchInsert * boolean value for useBulkCopyForBatchInsert. */ - void setUseBulkCopyForBatchInsert(boolean useBulkCopyForBatchInsert) ; - + void setUseBulkCopyForBatchInsert(boolean useBulkCopyForBatchInsert); + /** * Returns the useBulkCopyForBatchInsert value. * diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java index 8b4057b3e..a696e3490 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java @@ -30,7 +30,7 @@ class SQLServerBulkBatchInsertRecord extends SQLServerBulkRecord { */ private static final long serialVersionUID = -955998113956445541L; - private transient List batchParam; + transient List batchParam; private int batchParamIndex = -1; private List columnList; private List valueList; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java index 8b20c4f8f..c2f51aef5 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java @@ -2064,7 +2064,7 @@ private void writeNullToTdsWriter(TDSWriter tdsWriter, int srcJdbcType, private void writeColumnToTdsWriter(TDSWriter tdsWriter, int bulkPrecision, int bulkScale, int bulkJdbcType, boolean bulkNullable, // should it be destNullable instead? - int srcColOrdinal, int destColOrdinal, boolean isStreaming, Object colValue) throws SQLServerException { + int srcColOrdinal, int destColOrdinal, boolean isStreaming, Object colValue, Calendar cal) throws SQLServerException { SSType destSSType = destColumnMetadata.get(destColOrdinal).ssType; bulkPrecision = validateSourcePrecision(bulkPrecision, bulkJdbcType, @@ -2480,10 +2480,17 @@ else if (4 >= bulkScale) tdsWriter.writeByte((byte) 0x07); else tdsWriter.writeByte((byte) 0x08); - String timeStampValue = colValue.toString(); - tdsWriter.writeTime(java.sql.Timestamp.valueOf(timeStampValue), bulkScale); + + Timestamp ts; + if (colValue instanceof java.sql.Timestamp) { + ts = (Timestamp) colValue; + } else { + ts = Timestamp.valueOf(colValue.toString()); + } + + tdsWriter.writeTime(ts, bulkScale, cal); // Send only the date part - tdsWriter.writeDate(timeStampValue.substring(0, timeStampValue.lastIndexOf(' '))); + tdsWriter.writeDate(ts.getTime(), cal); } } break; @@ -2981,7 +2988,7 @@ private Object readColumnFromResultSet(int srcColOrdinal, int srcJdbcType, boole * Reads the given column from the result set current row and writes the data to tdsWriter. */ private void writeColumn(TDSWriter tdsWriter, int srcColOrdinal, int destColOrdinal, - Object colValue) throws SQLServerException { + Object colValue, Calendar cal) throws SQLServerException { String destName = destColumnMetadata.get(destColOrdinal).columnName; int srcPrecision, srcScale, destPrecision, srcJdbcType; SSType destSSType = null; @@ -3102,7 +3109,7 @@ else if (null != serverBulkData && (null == destCryptoMeta)) { } } writeColumnToTdsWriter(tdsWriter, srcPrecision, srcScale, srcJdbcType, srcNullable, srcColOrdinal, - destColOrdinal, isStreaming, colValue); + destColOrdinal, isStreaming, colValue, cal); } /** @@ -3633,7 +3640,7 @@ private boolean writeBatchData(TDSWriter tdsWriter, TDSCommand command, // Loop for each destination column. The mappings is a many to one mapping // where multiple source columns can be mapped to one destination column. for (ColumnMapping columnMapping : columnMappings) { - writeColumn(tdsWriter, columnMapping.sourceColumnOrdinal, columnMapping.destinationColumnOrdinal, + writeColumn(tdsWriter, columnMapping.sourceColumnOrdinal, columnMapping.destinationColumnOrdinal, null, null // cell // value is // retrieved @@ -3647,20 +3654,32 @@ private boolean writeBatchData(TDSWriter tdsWriter, TDSCommand command, else { // Get all the column values of the current row. Object[] rowObjects; + Parameter[] params = null; try { rowObjects = serverBulkData.getRowData(); + if (serverBulkData instanceof SQLServerBulkBatchInsertRecord) { + params = ((SQLServerBulkBatchInsertRecord) serverBulkData).batchParam.get(row); + } } catch (Exception ex) { // if no more data available to retrive throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveSourceData"), ex); } for (ColumnMapping columnMapping : columnMappings) { + + Object rowObject = rowObjects[columnMapping.sourceColumnOrdinal - 1]; + Calendar cal = null; + + if (rowObject instanceof Timestamp && params != null) { + cal = params[columnMapping.sourceColumnOrdinal - 1].getInputDTV().getCalendar(); + } + // If the SQLServerBulkCSVRecord does not have metadata for columns, it returns strings in the // object array. // COnvert the strings using destination table types. writeColumn(tdsWriter, columnMapping.sourceColumnOrdinal, columnMapping.destinationColumnOrdinal, - rowObjects[columnMapping.sourceColumnOrdinal - 1]); + rowObject, cal); } } row++; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java index 1bcb04529..f98585fe4 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java @@ -2589,7 +2589,8 @@ public void registerOutParameter(String parameterName, int sqlType) throws SQLSe loggerExternal.entering(getClassNameLogging(), "registerOutParameter", new Object[] {parameterName, sqlType}); checkClosed(); - registerOutParameterByName(findColumn(parameterName, CallableStatementGetterSetterMethod.IS_SETTER_METHOD), sqlType); + registerOutParameterByName(findColumn(parameterName, CallableStatementGetterSetterMethod.IS_SETTER_METHOD), + sqlType); loggerExternal.exiting(getClassNameLogging(), "registerOutParameter"); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java index 4c40b01c6..ffda2dd6b 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java @@ -712,6 +712,7 @@ public boolean getCalcBigDecimalPrecision() { public void setCalcBigDecimalPrecision(boolean calcBigDecimalPrecision) { wrappedConnection.setCalcBigDecimalPrecision(calcBigDecimalPrecision); } + /** * Returns the useBulkCopyForBatchInsert value. * @@ -719,7 +720,7 @@ public void setCalcBigDecimalPrecision(boolean calcBigDecimalPrecision) { */ @Override public boolean getUseBulkCopyForBatchInsert() { - return wrappedConnection.getUseBulkCopyForBatchInsert(); + return wrappedConnection.getUseBulkCopyForBatchInsert(); } /** @@ -730,6 +731,6 @@ public boolean getUseBulkCopyForBatchInsert() { */ @Override public void setUseBulkCopyForBatchInsert(boolean useBulkCopyForBatchInsert) { - wrappedConnection.setUseBulkCopyForBatchInsert(useBulkCopyForBatchInsert); + wrappedConnection.setUseBulkCopyForBatchInsert(useBulkCopyForBatchInsert); } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java index 35b27b310..2c45d1090 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java @@ -907,8 +907,8 @@ public final class SQLServerDriver implements java.sql.Driver { Boolean.toString(SQLServerDriverBooleanProperty.USE_DEFAULT_JAAS_CONFIG.getDefaultValue()), false, TRUE_FALSE), new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.CALC_BIG_DECIMAL_PRECISION.toString(), - Boolean.toString(SQLServerDriverBooleanProperty.CALC_BIG_DECIMAL_PRECISION.getDefaultValue()), false, - TRUE_FALSE), + Boolean.toString(SQLServerDriverBooleanProperty.CALC_BIG_DECIMAL_PRECISION.getDefaultValue()), + false, TRUE_FALSE), new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.SSL_PROTOCOL.toString(), SQLServerDriverStringProperty.SSL_PROTOCOL.getDefaultValue(), false, new String[] {SSLProtocol.TLS.toString(), SSLProtocol.TLS_V10.toString(), diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index bb8e49b9c..d5b9b5499 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -3422,6 +3422,7 @@ public final void setTimestamp(int n, java.sql.Timestamp x, java.util.Calendar c if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setTimestamp", new Object[] {n, x, cal}); checkClosed(); + setValue(n, JDBCType.TIMESTAMP, x, JavaType.TIMESTAMP, cal, false); loggerExternal.exiting(getClassNameLogging(), "setTimestamp"); } @@ -3433,6 +3434,7 @@ public final void setTimestamp(int n, java.sql.Timestamp x, java.util.Calendar c if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setTimestamp", new Object[] {n, x, cal, forceEncrypt}); checkClosed(); + setValue(n, JDBCType.TIMESTAMP, x, JavaType.TIMESTAMP, cal, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "setTimestamp"); } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java index 04fedc1ff..eabbbb574 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java @@ -209,7 +209,8 @@ public void testDataSource() throws SQLServerException { assertEquals(booleanPropValue, ds.getUseFlexibleCallableStatements(), TestResource.getResource("R_valuesAreDifferent")); ds.setCalcBigDecimalPrecision(booleanPropValue); - assertEquals(booleanPropValue, ds.getCalcBigDecimalPrecision(), TestResource.getResource("R_valuesAreDifferent")); + assertEquals(booleanPropValue, ds.getCalcBigDecimalPrecision(), + TestResource.getResource("R_valuesAreDifferent")); ds.setServerCertificate(stringPropValue); assertEquals(stringPropValue, ds.getServerCertificate(), TestResource.getResource("R_valuesAreDifferent")); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java index b19d25248..3712e9d1d 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java @@ -64,7 +64,7 @@ public void testBasicConnectionAAD() throws Exception { basicReconnect("jdbc:sqlserver://" + azureServer + ";database=" + azureDatabase + ";user=" + azureUserName + ";password=" + azurePassword + ";loginTimeout=90;Authentication=ActiveDirectoryPassword"); - retry = THROTTLE_RETRY_COUNT + 1; + retry = THROTTLE_RETRY_COUNT + 1; } catch (Exception e) { if (e.getMessage().matches(TestUtils.formatErrorMsg("R_crClientAllRecoveryAttemptsFailed"))) { System.out.println(e.getMessage() + ". Recovery failed, retry #" + retry + " in " diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java index 3482eff6b..3384f278c 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java @@ -13,11 +13,17 @@ import java.sql.BatchUpdateException; import java.sql.CallableStatement; import java.sql.Connection; +import java.sql.Date; +import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; import java.util.Arrays; +import java.util.Calendar; +import java.util.TimeZone; import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; import org.junit.jupiter.api.AfterAll; @@ -54,6 +60,10 @@ public class BatchExecutionTest extends AbstractTest { private static String ctstable3; private static String ctstable4; private static String ctstable3Procedure1; + private static String timestampTable1 = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("timestamptable1")); + private static String timestampTable2 = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("timestamptable2")); /** * This tests the updateCount when the error query does cause a SQL state HY008. @@ -99,6 +109,183 @@ public void testBatchUpdateCountTrueOnFirstPstmtSpPrepare() throws Exception { testBatchUpdateCountWith(5, 4, true, "prepare", expectedUpdateCount); } + @Test + public void testValidTimezoneForTimestampBatchInsertWithBulkCopy() throws Exception { + Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + long ms = 1578743412000L; + + // Insert Timestamp using batch insert + try (Connection con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement(); + PreparedStatement pstmt = con.prepareStatement("INSERT INTO " + timestampTable1 + " VALUES(?)")) { + + TestUtils.dropTableIfExists(timestampTable1, stmt); + String createSql = "CREATE TABLE " + timestampTable1 + " (c1 DATETIME2(3))"; + stmt.execute(createSql); + + Timestamp timestamp = new Timestamp(ms); + + pstmt.setTimestamp(1, timestamp, gmtCal); + pstmt.addBatch(); + pstmt.executeBatch(); + } + + // Insert Timestamp using bulkcopy for batch insert + try (Connection con = DriverManager.getConnection( + connectionString + ";useBulkCopyForBatchInsert=true;sendTemporalDataTypesAsStringForBulkCopy=false;"); + PreparedStatement pstmt = con.prepareStatement("INSERT INTO " + timestampTable1 + " VALUES(?)")) { + + Timestamp timestamp = new Timestamp(ms); + + pstmt.setTimestamp(1, timestamp, gmtCal); + pstmt.addBatch(); + pstmt.executeBatch(); + } + + // Compare Timestamp values inserted, should be the same + try (Connection con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM " + timestampTable1)) { + Timestamp ts0; + Timestamp ts1; + Time t0; + Time t1; + Date d0; + Date d1; + + rs.next(); + ts0 = rs.getTimestamp(1); + t0 = rs.getTime(1); + d0 = rs.getDate(1); + rs.next(); + ts1 = rs.getTimestamp(1); + t1 = rs.getTime(1); + d1 = rs.getDate(1); + + assertEquals(ts0, ts1); + assertEquals(t0, t1); + assertEquals(d0, d1); + } + } + + @Test + public void testValidTimezonesDstTimestampBatchInsertWithBulkCopy() throws Exception { + Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + + for (String tzId: TimeZone.getAvailableIDs()) { + TimeZone.setDefault(TimeZone.getTimeZone(tzId)); + + long ms = 1696127400000L; // DST + + // Insert Timestamp using batch insert + try (Connection con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement(); + PreparedStatement pstmt = con.prepareStatement("INSERT INTO " + timestampTable1 + " VALUES(?)")) { + + TestUtils.dropTableIfExists(timestampTable1, stmt); + String createSql = "CREATE TABLE " + timestampTable1 + " (c1 DATETIME2(3))"; + stmt.execute(createSql); + + Timestamp timestamp = new Timestamp(ms); + + pstmt.setTimestamp(1, timestamp, gmtCal); + pstmt.addBatch(); + pstmt.executeBatch(); + } + + // Insert Timestamp using bulkcopy for batch insert + try (Connection con = DriverManager.getConnection( + connectionString + ";useBulkCopyForBatchInsert=true;sendTemporalDataTypesAsStringForBulkCopy=false;"); + PreparedStatement pstmt = con.prepareStatement("INSERT INTO " + timestampTable1 + " VALUES(?)")) { + + Timestamp timestamp = new Timestamp(ms); + + pstmt.setTimestamp(1, timestamp, gmtCal); + pstmt.addBatch(); + pstmt.executeBatch(); + } + + // Compare Timestamp values inserted, should be the same + try (Connection con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM " + timestampTable1)) { + Timestamp ts0; + Timestamp ts1; + Time t0; + Time t1; + Date d0; + Date d1; + + rs.next(); + ts0 = rs.getTimestamp(1); + t0 = rs.getTime(1); + d0 = rs.getDate(1); + rs.next(); + ts1 = rs.getTimestamp(1); + t1 = rs.getTime(1); + d1 = rs.getDate(1); + + String failureMsg = "Failed for time zone: " + tzId; + assertEquals(ts0, ts1, failureMsg); + assertEquals(t0, t1, failureMsg); + assertEquals(d0, d1, failureMsg); + } + } + } + + @Test + public void testBatchInsertTimestampNoTimezoneDoubleConversion() throws Exception { + Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + long ms = 1578743412000L; + + // Insert Timestamp using prepared statement when useBulkCopyForBatchInsert=true + try (Connection con = DriverManager.getConnection(connectionString + + ";useBulkCopyForBatchInsert=true;sendTemporalDataTypesAsStringForBulkCopy=false;"); Statement stmt = con.createStatement(); + PreparedStatement pstmt = con.prepareStatement("INSERT INTO " + timestampTable2 + " VALUES(?)")) { + + TestUtils.dropTableIfExists(timestampTable2, stmt); + String createSql = "CREATE TABLE" + timestampTable2 + " (c1 DATETIME2(3))"; + stmt.execute(createSql); + + Timestamp timestamp = new Timestamp(ms); + + pstmt.setTimestamp(1, timestamp, gmtCal); + pstmt.execute(); + } + + // Insert Timestamp using bulkcopy for batch insert + try (Connection con = DriverManager.getConnection( + connectionString + ";useBulkCopyForBatchInsert=true;sendTemporalDataTypesAsStringForBulkCopy=false;"); + PreparedStatement pstmt = con.prepareStatement("INSERT INTO " + timestampTable2 + " VALUES(?)")) { + + Timestamp timestamp = new Timestamp(ms); + + pstmt.setTimestamp(1, timestamp, gmtCal); + pstmt.addBatch(); + pstmt.executeBatch(); + } + + // Compare Timestamp values inserted, should be the same + try (Connection con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM " + timestampTable2)) { + Timestamp ts0; + Timestamp ts1; + Time t0; + Time t1; + Date d0; + Date d1; + + rs.next(); + ts0 = rs.getTimestamp(1); + t0 = rs.getTime(1); + d0 = rs.getDate(1); + rs.next(); + ts1 = rs.getTimestamp(1); + t1 = rs.getTime(1); + d1 = rs.getDate(1); + + assertEquals(ts0, ts1); + assertEquals(t0, t1); + assertEquals(d0, d1); + } + } + /** * This tests the updateCount when the error query does not cause a SQL state HY008. * @@ -466,6 +653,9 @@ private static void dropTable() throws SQLException { TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(ctstable1), stmt); TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(ctstable3), stmt); TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(ctstable4), stmt); + TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(ctstable4), stmt); + TestUtils.dropTableIfExists(timestampTable1, stmt); + TestUtils.dropTableIfExists(timestampTable2, stmt); } } From 183426a7c4ff305342a976b471f9e094a44ac259 Mon Sep 17 00:00:00 2001 From: lilgreenbird Date: Mon, 29 Jan 2024 17:03:01 -0800 Subject: [PATCH 10/47] Fixed test to check for retryCount set in connection string (#2308) --- .../microsoft/sqlserver/jdbc/connection/TimeoutTest.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 49b05e713..af2bda8af 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java @@ -275,7 +275,12 @@ public void testAzureEndpointRetry() { } else if (TestUtils.isAzure(con)) { assertTrue(retryCount == 2); // AZURE_SERVER_ENDPOINT_RETRY_COUNT_DEFAFULT } else { - assertTrue(retryCount == 1); // default connectRetryCount + // default retryCount is 1 if not set in connection string + String retryCountFromConnStr = TestUtils.getProperty(connectionString, "connectRetryCount"); + int expectedRetryCount = (retryCountFromConnStr != null) ? Integer + .parseInt(retryCountFromConnStr) : 1; + + assertTrue(retryCount == expectedRetryCount); // default connectRetryCount } } } From 69bf8add538c2b172305666688e47c888dfc29d9 Mon Sep 17 00:00:00 2001 From: lilgreenbird Date: Tue, 30 Jan 2024 13:26:01 -0800 Subject: [PATCH 11/47] fix javadoc warnings (#2310) --- .../java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java | 1 + .../java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java | 2 ++ .../java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java | 1 + .../java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java | 1 + 4 files changed, 5 insertions(+) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java index c49b553ae..2f54a98ab 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java @@ -479,6 +479,7 @@ CallableStatement prepareCall(String sql, int nType, int nConcur, int nHold, * of the implementing class for {@link SQLServerAccessTokenCallback}. * * @param accessTokenCallbackClass + * access token callback class */ void setAccessTokenCallbackClass(String accessTokenCallbackClass); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java index 8f63e34c0..823b11d9d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java @@ -1331,6 +1331,8 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource { * of the implementing class for {@link SQLServerAccessTokenCallback}. * * @param accessTokenCallbackClass + * access token callback class + * */ void setAccessTokenCallbackClass(String accessTokenCallbackClass); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index c4563e091..afe161d30 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -8060,6 +8060,7 @@ public String getAccessTokenCallbackClass() { * of the implementing class for {@link SQLServerAccessTokenCallback}. * * @param accessTokenCallbackClass + * access token callback class */ public void setAccessTokenCallbackClass(String accessTokenCallbackClass) { this.accessTokenCallbackClass = accessTokenCallbackClass; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java index 54739dab5..7dfffdb51 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java @@ -1322,6 +1322,7 @@ public SQLServerAccessTokenCallback getAccessTokenCallback() { * of the implementing class for {@link SQLServerAccessTokenCallback}. * * @param accessTokenCallbackClass + * access token callback class */ @Override public void setAccessTokenCallbackClass(String accessTokenCallbackClass) { From 3cd946efe4e61446ba9ae073ecc0530746dee6a2 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Tue, 30 Jan 2024 13:34:20 -0800 Subject: [PATCH 12/47] 12.6.0 version update (#2307) * 12.6.0 misc update * Delete src/test/java/com/microsoft/sqlserver/jdbc/TestCl.java --- CHANGELOG.md | 30 ++++++++++++++----- README.md | 14 ++++----- build.gradle | 4 +-- mssql-jdbc_auth_LICENSE | 2 +- pom.xml | 4 +-- .../sqlserver/jdbc/SQLJdbcVersion.java | 4 +-- src/samples/adaptive/pom.xml | 2 +- src/samples/alwaysencrypted/pom.xml | 2 +- .../pom.xml | 2 +- src/samples/connections/pom.xml | 2 +- src/samples/constrained/pom.xml | 2 +- src/samples/dataclassification/pom.xml | 2 +- src/samples/datatypes/pom.xml | 2 +- src/samples/resultsets/pom.xml | 2 +- src/samples/sparse/pom.xml | 2 +- 15 files changed, 46 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eedd3bef..56107db32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,26 +3,42 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) +## [12.6.0] Stable Release +### Changed +- Adjusted PreparedStatement cache, so it's cleared before every execute [#2272](https://github.com/microsoft/mssql-jdbc/pull/2272) +- Updated azure-identity, azure-security-keyvault-keys, bouncycastle, and msal library versions [#2279](https://github.com/microsoft/mssql-jdbc/pull/2279) +- Changed `socketTimeout` to ensure it's always less than or equal to `loginTimeout` [#2280](https://github.com/microsoft/mssql-jdbc/pull/2280) +- Change BulkCopy behavior from serializing and deserializing Timestamp objects, to using the objects directly [#2291](https://github.com/microsoft/mssql-jdbc/pull/2291) + +### Fixed issues +- Fixed the way ActivityID was defined and used to be more in line with the behavior of other Microsoft drivers [#2254](https://github.com/microsoft/mssql-jdbc/pull/2254) +- - Fixed missing getters and setters for `useBulkCopyForBatchInsert` [#2277](https://github.com/microsoft/mssql-jdbc/pull/2277) +- - Fixed an issue where, when using the TOP qualifier in a query, the driver returns an error concerning ParameterMetadata [#2287](https://github.com/microsoft/mssql-jdbc/pull/2287) +- Fixed an issue where insert statements with missing whitespace worked correctly in regular cases, but not when using batch inserts [#2290](https://github.com/microsoft/mssql-jdbc/pull/2290) +- Fixed timezone not being properly applied to Timestamps when inserted using batch insert with bulkcopy [#2291](https://github.com/microsoft/mssql-jdbc/pull/2291) +- Fixed locks in IOBuffer to prevent deadlock issues that could arise [#2295](https://github.com/microsoft/mssql-jdbc/pull/2295) +- Fixed an issue where, when an exception has no cause, the exception itself is passed along instead, preventing it from being lost [#2300](https://github.com/microsoft/mssql-jdbc/pull/2300) + ## [12.5.0] Preview Release ### Added - Added connection property, `useDefaultJaasConfig`, to allow Kerberos authentication without any additional external configuration [#2147](https://github.com/microsoft/mssql-jdbc/pull/2147) -- Allow calling of stored procedures directly, simplifying the procedure and improving performance [#2154](https://github.com/microsoft/mssql-jdbc/pull/2154) +- Allow calling of stored procedures directly through use of new connection property `useFlexibleCallableStatements`, simplifying the procedure and improving performance [#2154](https://github.com/microsoft/mssql-jdbc/pull/2154) - Added connection property, `useDefaultGSSCredential`, to allow the driver to create GSSCredential on behalf of a user using Native GSS-API for Kerberos authentication [#2177](https://github.com/microsoft/mssql-jdbc/pull/2177) - Added Java 21 support [#2229](https://github.com/microsoft/mssql-jdbc/pull/2229) -- Added connection property, `calcBigDecimalScale`, to allow the driver to calculate scale and percision from Big Decimal inputs [#2248](https://github.com/microsoft/mssql-jdbc/pull/2248) +- Added connection property, `calcBigDecimalScale`, to allow the driver to calculate scale and precision from Big Decimal inputs [#2248](https://github.com/microsoft/mssql-jdbc/pull/2248) - Added a new named logger for connection open retries and idle connection resiliency reconnects [#2250](https://github.com/microsoft/mssql-jdbc/pull/2250) ### Changed -- Changed how IBM JDK is checked for to prevent issues with OSGi environments [#2150](https://github.com/microsoft/mssql-jdbc/pull/2150) -- Updated azure-security-keyvault-keys, bouncycastle, and h2 library versions [#2162](https://github.com/microsoft/mssql-jdbc/pull/2162)[#2182](https://github.com/microsoft/mssql-jdbc/pull/2182)[#2249](https://github.com/microsoft/mssql-jdbc/pull/2249) +- Changed how IBM JDK is checked for to prevent issues with OSGi environments [#2150](https://github.com/microsoft/mssql-jdbc/pull/2150)[#2209](https://github.com/microsoft/mssql-jdbc/pull/2209) +- Updated azure-security-keyvault-keys, bouncycastle, and h2 library versions. As well, Upgraded from `bcprov-jdk15on` and `bcpkix-jdk15on` to `bcprov-jdk18on` and `bcpkix-jdk18on` as the former is no longer being updated [#2162](https://github.com/microsoft/mssql-jdbc/pull/2162)[#2182](https://github.com/microsoft/mssql-jdbc/pull/2182)[#2249](https://github.com/microsoft/mssql-jdbc/pull/2249) - Changes to bulkcopy to allow for performance improvements when loading a large number of timestamps [#2194](https://github.com/microsoft/mssql-jdbc/pull/2194) - Added additional errors that should translate to RMFAIL [#2201](https://github.com/microsoft/mssql-jdbc/pull/2201) -- Properly synchronize all calls to MSAL, preventing the driver from making extra calls and providing unneccessary dialogues [#2218](https://github.com/microsoft/mssql-jdbc/pull/2218) -- Changed driver retry behavior to retry the correct number of times based on connectRetryCount [#2247](https://github.com/microsoft/mssql-jdbc/pull/2247) +- Properly synchronize all calls to MSAL, preventing the driver from making extra calls and providing unnecessary dialogues [#2218](https://github.com/microsoft/mssql-jdbc/pull/2218) +- Changed driver retry behavior to retry the correct number of times based on connectRetryCount. These changes were later reverted prior to the 12.6.0 release [#2247](https://github.com/microsoft/mssql-jdbc/pull/2247)[#2267](https://github.com/microsoft/mssql-jdbc/pull/2267) ### Fixed issues - Fix to ignore irrelevant computed columns during bulk insert [#1562](https://github.com/microsoft/mssql-jdbc/pull/1562) -- Fixed an issue where signature was not properly verfied when using Java Key Store, as well as adding a new API to sign column master key metadata (and return generated signature) for use with Java Key Store and Azure Key Vault [#2160](https://github.com/microsoft/mssql-jdbc/pull/2160) +- Fixed an issue where signature was not properly verified when using Java Key Store, as well as adding a new API to sign column master key metadata (and return generated signature) for use with Java Key Store and Azure Key Vault [#2160](https://github.com/microsoft/mssql-jdbc/pull/2160) - Fixed an issue where a null SQLState was returned when trying to convert a date to a long [#2185](https://github.com/microsoft/mssql-jdbc/pull/2185) - Fixed an issue where schemaPattern was not properly being escaped in SQLServerDatabaseMetadata [#2195](https://github.com/microsoft/mssql-jdbc/pull/2195) - Fixes getObject()'s erroneous conversion of DateTimeOffset to LocalDateTime [#2204](https://github.com/microsoft/mssql-jdbc/pull/2204) diff --git a/README.md b/README.md index 6befca937..4f3f01687 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ We're now on the Maven Central Repository. Add the following to your POM file to com.microsoft.sqlserver mssql-jdbc - 12.4.1.jre11 + 12.6.0.jre11 ``` The driver can be downloaded from [Microsoft](https://aka.ms/downloadmssqljdbc). For driver version 12.1.0 and greater, please use the jre11 version when using Java 11 or greater, and the jre8 version when using Java 8. @@ -92,7 +92,7 @@ To get the latest version of the driver, add the following to your POM file: com.microsoft.sqlserver mssql-jdbc - 12.4.1.jre11 + 12.6.0.jre11 ``` @@ -106,7 +106,7 @@ This project has following dependencies: Compile Time: - `com.azure:azure-security-keyvault-keys` : Microsoft Azure Client Library For KeyVault Keys (optional) - `com.azure:azure-identity` : Microsoft Azure Client Library For Identity (optional) - - `org.bouncycastle:bcprov-jdk15on` : Bouncy Castle Provider for Always Encrypted with secure enclaves feature with JAVA 8 only (optional) + - `org.bouncycastle:bcprov-jdk18on` : Bouncy Castle Provider for Always Encrypted with secure enclaves feature with JAVA 8 only (optional) - `com.google.code.gson:gson` : Gson for Always Encrypted with secure enclaves feature (optional) Test Time: @@ -127,7 +127,7 @@ Projects that require either of the two features need to explicitly declare the com.microsoft.sqlserver mssql-jdbc - 12.4.1.jre11 + 12.6.0.jre11 compile @@ -145,7 +145,7 @@ Projects that require either of the two features need to explicitly declare the com.microsoft.sqlserver mssql-jdbc - 12.4.1.jre11 + 12.6.0.jre11 compile @@ -172,7 +172,7 @@ When setting 'useFmtOnly' property to 'true' for establishing a connection or cr com.microsoft.sqlserver mssql-jdbc - 12.4.1.jre11 + 12.6.0.jre11 @@ -212,7 +212,7 @@ Preview releases happen approximately monthly between stable releases. This give You can see what is going into a future release by monitoring [Milestones](https://github.com/Microsoft/mssql-jdbc/milestones) in the repository. ### Version conventions -Starting with 6.0, stable versions have an even minor version. For example, 6.0, 6.2, 6.4, 7.0, 7.2, 7.4, 8.2, 8.4, 9.2, 9.4, 10.2, 11.2, 12.2, 12.4. Preview versions have an odd minor version. For example, 6.1, 6.3, 6.5, 7.1, 7.3, 8.1, 9.1, 10.1, 11.1, 12.1, 12.3, and so on. +Starting with 6.0, stable versions have an even minor version. For example, 6.0, 6.2, 6.4, 7.0, 7.2, 7.4, 8.2, 8.4, 9.2, 9.4, 10.2, 11.2, 12.2, 12.4, 12.6. Preview versions have an odd minor version. For example, 6.1, 6.3, 6.5, 7.1, 7.3, 8.1, 9.1, 10.1, 11.1, 12.1, 12.3, 12.5, and so on. ## Contributors Special thanks to everyone who has contributed to the project. diff --git a/build.gradle b/build.gradle index 310dcfc35..eb40c3013 100644 --- a/build.gradle +++ b/build.gradle @@ -11,8 +11,8 @@ apply plugin: 'java' -version = '12.5.0' -def releaseExt = '-preview' +version = '12.6.0' +def releaseExt = '' def jreVersion = "" def testOutputDir = file("build/classes/java/test") def archivesBaseName = 'mssql-jdbc' diff --git a/mssql-jdbc_auth_LICENSE b/mssql-jdbc_auth_LICENSE index e5f8973c2..eddb50467 100644 --- a/mssql-jdbc_auth_LICENSE +++ b/mssql-jdbc_auth_LICENSE @@ -1,5 +1,5 @@ MICROSOFT SOFTWARE LICENSE TERMS -MICROSOFT JDBC DRIVER 12.4.0 FOR SQL SERVER +MICROSOFT JDBC DRIVER 12.6.0 FOR SQL SERVER These license terms are an agreement between you and Microsoft Corporation (or one of its affiliates). They apply to the software named above and any Microsoft services or software updates (except to the extent such services or updates are accompanied by new or additional terms, in which case those different terms apply prospectively and do not alter your or Microsoft’s rights relating to pre-updated software or services). IF YOU COMPLY WITH THESE LICENSE TERMS, YOU HAVE THE RIGHTS BELOW. BY USING THE SOFTWARE, YOU ACCEPT THESE TERMS. diff --git a/pom.xml b/pom.xml index a87128a09..60d749593 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.microsoft.sqlserver mssql-jdbc - 12.5.0 + 12.6.0 jar Microsoft JDBC Driver for SQL Server @@ -51,7 +51,7 @@ Default testing enabled with SQL Server 2019 (SQLv15) --> xSQLv12,xSQLv15,NTLM,MSI,reqExternalSetup,clientCertAuth,fedAuth,kerberos - -preview + 6.0.0 4.7.3 diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java index 8adbb06fc..78b11810d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java @@ -7,7 +7,7 @@ final class SQLJdbcVersion { static final int MAJOR = 12; - static final int MINOR = 5; + static final int MINOR = 6; static final int PATCH = 0; static final int BUILD = 0; /* @@ -15,7 +15,7 @@ final class SQLJdbcVersion { * 1. Set to "-preview" for preview release. * 2. Set to "" (empty String) for official release. */ - static final String RELEASE_EXT = "-preview"; + static final String RELEASE_EXT = ""; private SQLJdbcVersion() { throw new UnsupportedOperationException(SQLServerException.getErrString("R_notSupported")); diff --git a/src/samples/adaptive/pom.xml b/src/samples/adaptive/pom.xml index 96e2be9a0..f1ce6c7d4 100644 --- a/src/samples/adaptive/pom.xml +++ b/src/samples/adaptive/pom.xml @@ -15,7 +15,7 @@ com.microsoft.sqlserver mssql-jdbc - 12.4.0.jre11 + 12.6.0.jre11 diff --git a/src/samples/alwaysencrypted/pom.xml b/src/samples/alwaysencrypted/pom.xml index 294d4b419..b26a55e0c 100644 --- a/src/samples/alwaysencrypted/pom.xml +++ b/src/samples/alwaysencrypted/pom.xml @@ -15,7 +15,7 @@ com.microsoft.sqlserver mssql-jdbc - 12.4.0.jre11 + 12.6.0.jre11 diff --git a/src/samples/azureactivedirectoryauthentication/pom.xml b/src/samples/azureactivedirectoryauthentication/pom.xml index 518da61a7..d704cd96f 100644 --- a/src/samples/azureactivedirectoryauthentication/pom.xml +++ b/src/samples/azureactivedirectoryauthentication/pom.xml @@ -14,7 +14,7 @@ com.microsoft.sqlserver mssql-jdbc - 12.4.0.jre11 + 12.6.0.jre11 diff --git a/src/samples/connections/pom.xml b/src/samples/connections/pom.xml index 209697c3c..7bc8fd947 100644 --- a/src/samples/connections/pom.xml +++ b/src/samples/connections/pom.xml @@ -14,7 +14,7 @@ com.microsoft.sqlserver mssql-jdbc - 12.4.0.jre11 + 12.6.0.jre11 diff --git a/src/samples/constrained/pom.xml b/src/samples/constrained/pom.xml index c2cc82ae6..9518b2c53 100644 --- a/src/samples/constrained/pom.xml +++ b/src/samples/constrained/pom.xml @@ -16,7 +16,7 @@ com.microsoft.sqlserver mssql-jdbc - 12.4.0.jre11 + 12.6.0.jre11 diff --git a/src/samples/dataclassification/pom.xml b/src/samples/dataclassification/pom.xml index 74ba22bb0..f4f9f7ce4 100644 --- a/src/samples/dataclassification/pom.xml +++ b/src/samples/dataclassification/pom.xml @@ -16,7 +16,7 @@ com.microsoft.sqlserver mssql-jdbc - 12.4.0.jre11 + 12.6.0.jre11 diff --git a/src/samples/datatypes/pom.xml b/src/samples/datatypes/pom.xml index 148d73819..264dbb745 100644 --- a/src/samples/datatypes/pom.xml +++ b/src/samples/datatypes/pom.xml @@ -15,7 +15,7 @@ com.microsoft.sqlserver mssql-jdbc - 12.4.0.jre11 + 12.6.0.jre11 diff --git a/src/samples/resultsets/pom.xml b/src/samples/resultsets/pom.xml index d466f5e75..979b36010 100644 --- a/src/samples/resultsets/pom.xml +++ b/src/samples/resultsets/pom.xml @@ -14,7 +14,7 @@ com.microsoft.sqlserver mssql-jdbc - 12.4.0.jre11 + 12.6.0.jre11 diff --git a/src/samples/sparse/pom.xml b/src/samples/sparse/pom.xml index e6e3dd4de..9df693086 100644 --- a/src/samples/sparse/pom.xml +++ b/src/samples/sparse/pom.xml @@ -14,7 +14,7 @@ com.microsoft.sqlserver mssql-jdbc - 12.4.0.jre11 + 12.6.0.jre11 From 9b5036d9d8d2bb4cac8a32b984d905a93f084ad5 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Fri, 2 Feb 2024 12:25:07 -0800 Subject: [PATCH 13/47] Updated versions for 12.7 (#2317) --- build.gradle | 4 ++-- pom.xml | 4 ++-- .../java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index eb40c3013..97b53a4d4 100644 --- a/build.gradle +++ b/build.gradle @@ -11,8 +11,8 @@ apply plugin: 'java' -version = '12.6.0' -def releaseExt = '' +version = '12.7.0' +def releaseExt = '-preview' def jreVersion = "" def testOutputDir = file("build/classes/java/test") def archivesBaseName = 'mssql-jdbc' diff --git a/pom.xml b/pom.xml index 60d749593..642cc6490 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.microsoft.sqlserver mssql-jdbc - 12.6.0 + 12.7.0-SNAPSHOT jar Microsoft JDBC Driver for SQL Server @@ -51,7 +51,7 @@ Default testing enabled with SQL Server 2019 (SQLv15) --> xSQLv12,xSQLv15,NTLM,MSI,reqExternalSetup,clientCertAuth,fedAuth,kerberos - + -preview 6.0.0 4.7.3 diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java index 78b11810d..8cb90d58d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java @@ -7,7 +7,7 @@ final class SQLJdbcVersion { static final int MAJOR = 12; - static final int MINOR = 6; + static final int MINOR = 7; static final int PATCH = 0; static final int BUILD = 0; /* @@ -15,7 +15,7 @@ final class SQLJdbcVersion { * 1. Set to "-preview" for preview release. * 2. Set to "" (empty String) for official release. */ - static final String RELEASE_EXT = ""; + static final String RELEASE_EXT = "-preview"; private SQLJdbcVersion() { throw new UnsupportedOperationException(SQLServerException.getErrString("R_notSupported")); From e352d89b8cd785f3170402697ca97ebbd456e608 Mon Sep 17 00:00:00 2001 From: lilgreenbird Date: Thu, 15 Feb 2024 14:25:04 -0800 Subject: [PATCH 14/47] Update CodeQL action versions (#2327) --- .github/workflows/codeql.yml | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index dc42ea428..c4908ce5a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,14 +1,3 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# name: "CodeQL" on: @@ -18,8 +7,6 @@ on: pull_request: # The branches below must be a subset of the branches above branches: [ "main" ] - schedule: - - cron: '27 5 * * 2' jobs: analyze: @@ -34,16 +21,14 @@ jobs: fail-fast: false matrix: language: [ 'java' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -56,7 +41,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) #- name: Autobuild - # uses: github/codeql-action/autobuild@v2 + # uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -67,9 +52,10 @@ jobs: # - run: | # echo "Run, Build Application using script" # ./location_of_script_within_repo/buildscript.sh + - run: mvn install -Pjre11 -Denforcer.skip -Dmaven.javadoc.skip -DskipTests -Dmaven.test.skip.exec - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" From ba88da8e97d10b2ffda325599699a37525981ffd Mon Sep 17 00:00:00 2001 From: Terry Chow <32403408+tkyc@users.noreply.github.com> Date: Thu, 15 Feb 2024 14:26:30 -0800 Subject: [PATCH 15/47] Re-added support for stored procedure 'exec' escape syntax in CallableStatements (#2325) * EXEC system stored procedure regression fix * Additional test * Additional test * Indenting * Switched error string to TestResource error string * CR comments * Test update p1 * Test update p2 * CR comment changes; Test update * call escape syntax check * CR changes * formatting --- .../sqlserver/jdbc/SQLServerBulkCopy.java | 11 ++- .../jdbc/SQLServerPreparedStatement.java | 37 +++++++- .../CallableStatementTest.java | 95 +++++++++++++++++-- .../unit/statement/BatchExecutionTest.java | 11 ++- 4 files changed, 137 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java index c2f51aef5..e5fa8825e 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java @@ -2064,7 +2064,8 @@ private void writeNullToTdsWriter(TDSWriter tdsWriter, int srcJdbcType, private void writeColumnToTdsWriter(TDSWriter tdsWriter, int bulkPrecision, int bulkScale, int bulkJdbcType, boolean bulkNullable, // should it be destNullable instead? - int srcColOrdinal, int destColOrdinal, boolean isStreaming, Object colValue, Calendar cal) throws SQLServerException { + int srcColOrdinal, int destColOrdinal, boolean isStreaming, Object colValue, + Calendar cal) throws SQLServerException { SSType destSSType = destColumnMetadata.get(destColOrdinal).ssType; bulkPrecision = validateSourcePrecision(bulkPrecision, bulkJdbcType, @@ -2987,8 +2988,8 @@ private Object readColumnFromResultSet(int srcColOrdinal, int srcJdbcType, boole /** * Reads the given column from the result set current row and writes the data to tdsWriter. */ - private void writeColumn(TDSWriter tdsWriter, int srcColOrdinal, int destColOrdinal, - Object colValue, Calendar cal) throws SQLServerException { + private void writeColumn(TDSWriter tdsWriter, int srcColOrdinal, int destColOrdinal, Object colValue, + Calendar cal) throws SQLServerException { String destName = destColumnMetadata.get(destColOrdinal).columnName; int srcPrecision, srcScale, destPrecision, srcJdbcType; SSType destSSType = null; @@ -3640,8 +3641,8 @@ private boolean writeBatchData(TDSWriter tdsWriter, TDSCommand command, // Loop for each destination column. The mappings is a many to one mapping // where multiple source columns can be mapped to one destination column. for (ColumnMapping columnMapping : columnMappings) { - writeColumn(tdsWriter, columnMapping.sourceColumnOrdinal, columnMapping.destinationColumnOrdinal, null, - null // cell + writeColumn(tdsWriter, columnMapping.sourceColumnOrdinal, columnMapping.destinationColumnOrdinal, + null, null // cell // value is // retrieved // inside diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index d5b9b5499..7789b3726 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -29,6 +29,7 @@ import java.util.Map.Entry; import java.util.Vector; import java.util.logging.Level; +import java.util.regex.Pattern; import com.microsoft.sqlserver.jdbc.SQLServerConnection.CityHash128Key; import com.microsoft.sqlserver.jdbc.SQLServerConnection.PreparedStatementHandle; @@ -70,6 +71,10 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements IS /** Processed SQL statement text, may not be same as what user initially passed. */ final String userSQL; + private boolean isExecEscapeSyntax; + + private boolean isCallEscapeSyntax; + /** Parameter positions in processed SQL statement text. */ final int[] userSQLParamPositions; @@ -128,6 +133,17 @@ private void setPreparedStatementHandle(int handle) { */ private boolean useBulkCopyForBatchInsert; + /** + * Regex for JDBC 'call' escape syntax + */ + private static final Pattern callEscapePattern = Pattern + .compile("^\\s*(?i)\\{(\\s*\\??\\s*=?\\s*)call (.+)\\s*\\(?\\?*,?\\)?\\s*}\\s*$"); + + /** + * Regex for 'exec' escape syntax + */ + private static final Pattern execEscapePattern = Pattern.compile("^\\s*(?i)(?:exec|execute)\\b"); + /** Returns the prepared statement SQL */ @Override public String toString() { @@ -253,6 +269,8 @@ private boolean resetPrepStmtHandle(boolean discardCurrentCacheItem) { procedureName = parsedSQL.procedureName; bReturnValueSyntax = parsedSQL.bReturnValueSyntax; userSQL = parsedSQL.processedSQL; + isExecEscapeSyntax = isExecEscapeSyntax(sql); + isCallEscapeSyntax = isCallEscapeSyntax(sql); userSQLParamPositions = parsedSQL.parameterPositions; initParams(userSQLParamPositions.length); useBulkCopyForBatchInsert = conn.getUseBulkCopyForBatchInsert(); @@ -1210,7 +1228,16 @@ else if (needsPrepare && !connection.getEnablePrepareOnFirstPreparedStatementCal */ boolean callRPCDirectly(Parameter[] params) throws SQLServerException { int paramCount = SQLServerConnection.countParams(userSQL); - return (null != procedureName && paramCount != 0 && !isTVPType(params)); + + // In order to execute sprocs directly the following must be true: + // 1. There must be a sproc name + // 2. There must be parameters + // 3. Parameters must not be a TVP type + // 4. Compliant CALL escape syntax + // If isExecEscapeSyntax is true, EXEC escape syntax is used then use prior behaviour to + // execute the procedure + return (null != procedureName && paramCount != 0 && !isTVPType(params) && isCallEscapeSyntax + && !isExecEscapeSyntax); } /** @@ -1230,6 +1257,14 @@ private boolean isTVPType(Parameter[] params) throws SQLServerException { return false; } + private boolean isExecEscapeSyntax(String sql) { + return execEscapePattern.matcher(sql).find(); + } + + private boolean isCallEscapeSyntax(String sql) { + return callEscapePattern.matcher(sql).find(); + } + /** * Executes sp_prepare to prepare a parameterized statement and sets the prepared statement handle * 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 3a480548d..6570abf72 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java @@ -81,7 +81,7 @@ public class CallableStatementTest extends AbstractTest { /** * Setup before test - * + * * @throws SQLException */ @BeforeAll @@ -201,7 +201,7 @@ public void testCallableStatementSpPrepare() throws SQLException { /** * Tests CallableStatement.getString() with uniqueidentifier parameter - * + * * @throws SQLException */ @Test @@ -226,7 +226,7 @@ public void getStringGUIDTest() throws SQLException { /** * test for setNull(index, varchar) to behave as setNull(index, nvarchar) when SendStringParametersAsUnicode is true - * + * * @throws SQLException */ @Test @@ -302,7 +302,7 @@ public void testGetObjectAsLocalDateTime() throws SQLException { /** * Tests getObject(n, java.time.OffsetDateTime.class) and getObject(n, java.time.OffsetTime.class). - * + * * @throws SQLException */ @Test @@ -332,7 +332,7 @@ public void testGetObjectAsOffsetDateTime() throws SQLException { /** * recognize parameter names with and without leading '@' - * + * * @throws SQLException */ @Test @@ -1067,9 +1067,92 @@ public void testRegisteringOutputByIndexandAcquiringOutputParamByName() throws S } } + @Test + public void testExecuteSystemStoredProcedureNamedParametersAndIndexedParameterNoResultset() throws SQLException { + String call0 = "EXEC sp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'"; + String call1 = "\rEXEC\r\rsp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'"; + String call2 = " EXEC sp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'"; + String call3 = "\tEXEC\t\t\tsp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'"; + + try (CallableStatement cstmt0 = connection.prepareCall(call0); + CallableStatement cstmt1 = connection.prepareCall(call1); + CallableStatement cstmt2 = connection.prepareCall(call2); + CallableStatement cstmt3 = connection.prepareCall(call3);) { + cstmt0.setString(1, "Resource-" + UUID.randomUUID()); + cstmt0.execute(); + + cstmt1.setString(1, "Resource-" + UUID.randomUUID()); + cstmt1.execute(); + + cstmt2.setString(1, "Resource-" + UUID.randomUUID()); + cstmt2.execute(); + + cstmt3.setString(1, "Resource-" + UUID.randomUUID()); + cstmt3.execute(); + } + } + + @Test + public void testExecSystemStoredProcedureNamedParametersAndIndexedParameterResultSet() throws SQLException { + String call = "exec sp_sproc_columns_100 ?, @ODBCVer=3, @fUsePattern=0"; + + try (CallableStatement cstmt = connection.prepareCall(call)) { + cstmt.setString(1, "sp_getapplock"); + + try (ResultSet rs = cstmt.executeQuery()) { + while (rs.next()) { + assertTrue(TestResource.getResource("R_resultSetEmpty"), !rs.getString(4).isEmpty()); + } + } + } + } + + @Test + public void testExecSystemStoredProcedureNoIndexedParametersResultSet() throws SQLException { + String call = "execute sp_sproc_columns_100 sp_getapplock, @ODBCVer=3, @fUsePattern=0"; + + try (CallableStatement cstmt = connection.prepareCall(call); ResultSet rs = cstmt.executeQuery()) { + while (rs.next()) { + assertTrue(TestResource.getResource("R_resultSetEmpty"), !rs.getString(4).isEmpty()); + } + } + } + + @Test + public void testExecDocumentedSystemStoredProceduresIndexedParameters() throws SQLException { + String serverName; + String testTableName = "testTable"; + Integer integer = new Integer(1); + + try (Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery("SELECT @@SERVERNAME")) { + rs.next(); + serverName = rs.getString(1); + } + + String[] sprocs = {"EXEC sp_column_privileges ?", "exec sp_catalogs ?", "execute sp_column_privileges ?", + "EXEC sp_column_privileges_ex ?", "EXECUTE sp_columns ?", "execute sp_datatype_info ?", + "EXEC sp_sproc_columns ?", "EXECUTE sp_server_info ?", "exec sp_special_columns ?", + "execute sp_statistics ?", "EXEC sp_table_privileges ?", "exec sp_tables ?"}; + + Object[] params = {testTableName, serverName, testTableName, serverName, testTableName, integer, + "sp_column_privileges", integer, testTableName, testTableName, testTableName, testTableName}; + + int paramIndex = 0; + + for (String sproc : sprocs) { + try (CallableStatement cstmt = connection.prepareCall(sproc)) { + cstmt.setObject(1, params[paramIndex]); + cstmt.execute(); + paramIndex++; + } catch (Exception e) { + fail("Failed executing '" + sproc + "' with indexed parameter '" + params[paramIndex]); + } + } + } + /** * Cleanup after test - * + * * @throws SQLException */ @AfterAll diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java index 3384f278c..0dace62b2 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java @@ -170,7 +170,7 @@ public void testValidTimezoneForTimestampBatchInsertWithBulkCopy() throws Except public void testValidTimezonesDstTimestampBatchInsertWithBulkCopy() throws Exception { Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); - for (String tzId: TimeZone.getAvailableIDs()) { + for (String tzId : TimeZone.getAvailableIDs()) { TimeZone.setDefault(TimeZone.getTimeZone(tzId)); long ms = 1696127400000L; // DST @@ -191,8 +191,8 @@ public void testValidTimezonesDstTimestampBatchInsertWithBulkCopy() throws Excep } // Insert Timestamp using bulkcopy for batch insert - try (Connection con = DriverManager.getConnection( - connectionString + ";useBulkCopyForBatchInsert=true;sendTemporalDataTypesAsStringForBulkCopy=false;"); + try (Connection con = DriverManager.getConnection(connectionString + + ";useBulkCopyForBatchInsert=true;sendTemporalDataTypesAsStringForBulkCopy=false;"); PreparedStatement pstmt = con.prepareStatement("INSERT INTO " + timestampTable1 + " VALUES(?)")) { Timestamp timestamp = new Timestamp(ms); @@ -235,8 +235,9 @@ public void testBatchInsertTimestampNoTimezoneDoubleConversion() throws Exceptio long ms = 1578743412000L; // Insert Timestamp using prepared statement when useBulkCopyForBatchInsert=true - try (Connection con = DriverManager.getConnection(connectionString - + ";useBulkCopyForBatchInsert=true;sendTemporalDataTypesAsStringForBulkCopy=false;"); Statement stmt = con.createStatement(); + try (Connection con = DriverManager.getConnection( + connectionString + ";useBulkCopyForBatchInsert=true;sendTemporalDataTypesAsStringForBulkCopy=false;"); + Statement stmt = con.createStatement(); PreparedStatement pstmt = con.prepareStatement("INSERT INTO " + timestampTable2 + " VALUES(?)")) { TestUtils.dropTableIfExists(timestampTable2, stmt); From cfb018a62ebc0cf4a7fadac6ffaa282467fc44dd Mon Sep 17 00:00:00 2001 From: lilgreenbird Date: Wed, 21 Feb 2024 17:11:42 -0800 Subject: [PATCH 16/47] remove synchronized (#2337) --- .../com/microsoft/sqlserver/jdbc/IOBuffer.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index e5e175b9e..3a5db2bf8 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -1440,7 +1440,7 @@ public int getPort() { } @Override - public synchronized int getReceiveBufferSize() throws SocketException { + public int getReceiveBufferSize() throws SocketException { return tdsChannel.tcpSocket.getReceiveBufferSize(); } @@ -1455,7 +1455,7 @@ public boolean getReuseAddress() throws SocketException { } @Override - public synchronized int getSendBufferSize() throws SocketException { + public int getSendBufferSize() throws SocketException { return tdsChannel.tcpSocket.getSendBufferSize(); } @@ -1465,7 +1465,7 @@ public int getSoLinger() throws SocketException { } @Override - public synchronized int getSoTimeout() throws SocketException { + public int getSoTimeout() throws SocketException { return tdsChannel.tcpSocket.getSoTimeout(); } @@ -1536,19 +1536,19 @@ public void connect(SocketAddress endpoint, int timeout) throws IOException { // Ignore calls to methods that would otherwise allow the SSL socket // to directly manipulate the underlying TCP socket @Override - public synchronized void close() throws IOException { + public void close() throws IOException { if (logger.isLoggable(Level.FINER)) logger.finer(logContext + " Ignoring close"); } @Override - public synchronized void setReceiveBufferSize(int size) throws SocketException { + public void setReceiveBufferSize(int size) throws SocketException { if (logger.isLoggable(Level.FINER)) logger.finer(toString() + " Ignoring setReceiveBufferSize size:" + size); } @Override - public synchronized void setSendBufferSize(int size) throws SocketException { + public void setSendBufferSize(int size) throws SocketException { if (logger.isLoggable(Level.FINER)) logger.finer(toString() + " Ignoring setSendBufferSize size:" + size); } @@ -1566,7 +1566,7 @@ public void setSoLinger(boolean on, int linger) throws SocketException { } @Override - public synchronized void setSoTimeout(int timeout) throws SocketException { + public void setSoTimeout(int timeout) throws SocketException { tdsChannel.tcpSocket.setSoTimeout(timeout); } From dc191dbec9d3f75541da0549c201c79350d72dd1 Mon Sep 17 00:00:00 2001 From: lilgreenbird Date: Wed, 28 Feb 2024 16:17:59 -0800 Subject: [PATCH 17/47] Fix to allow connection retries to be disabled by setting connectRetryCount to 0 (#2293) --- .../sqlserver/jdbc/SQLServerConnection.java | 144 ++++---- .../sqlserver/jdbc/SQLServerException.java | 19 +- .../jdbc/SQLServerConnectionTest.java | 43 ++- .../jdbc/connection/TimeoutTest.java | 314 ++++++++---------- 4 files changed, 264 insertions(+), 256 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index afe161d30..982cf0e8e 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -3222,6 +3222,21 @@ else if (0 == requestedPacketSize) return this; } + // log open connection failures + private void logConnectFailure(int attemptNumber, SQLServerException e, SQLServerError sqlServerError) { + loggerResiliency.finer(toString() + " Connection open - connection failed on attempt: " + attemptNumber + "."); + + if (e != null) { + loggerResiliency.finer( + toString() + " Connection open - connection failure. Driver error code: " + e.getDriverErrorCode()); + } + + if (null != sqlServerError && !sqlServerError.getErrorMessage().isEmpty()) { + loggerResiliency.finer(toString() + " Connection open - connection failure. SQL Server error : " + + sqlServerError.getErrorMessage()); + } + } + /** * This function is used by non failover and failover cases. Even when we make a standard connection the server can * provide us with its FO partner. If no FO information is available a standard connection is made. If the server @@ -3237,6 +3252,7 @@ private void login(String primary, String primaryInstanceName, int primaryPortNu int fedauthRetryInterval = BACKOFF_INTERVAL; // milliseconds to sleep (back off) between attempts. long timeoutUnitInterval; + long timeForFirstTry = 0; // time it took to do 1st try in ms boolean useFailoverHost = false; FailoverInfo tempFailover = null; @@ -3434,34 +3450,24 @@ private void login(String primary, String primaryInstanceName, int primaryPortNu + connectRetryCount + " reached."); } - int errorCode = e.getErrorCode(); - int driverErrorCode = e.getDriverErrorCode(); + // estimate time it took to do 1 try + if (attemptNumber == 0) { + timeForFirstTry = (System.currentTimeMillis() - timerStart); + } + sqlServerError = e.getSQLServerError(); - if (SQLServerException.LOGON_FAILED == errorCode // logon failed, ie bad password - || SQLServerException.PASSWORD_EXPIRED == errorCode // password expired - || SQLServerException.USER_ACCOUNT_LOCKED == errorCode // user account locked - || SQLServerException.DRIVER_ERROR_INVALID_TDS == driverErrorCode // invalid TDS - || SQLServerException.DRIVER_ERROR_SSL_FAILED == driverErrorCode // SSL failure - || SQLServerException.DRIVER_ERROR_INTERMITTENT_TLS_FAILED == driverErrorCode // TLS1.2 failure - || SQLServerException.DRIVER_ERROR_UNSUPPORTED_CONFIG == driverErrorCode // unsupported config - // (eg Sphinx, invalid - // packetsize, etc) - || (SQLServerException.ERROR_SOCKET_TIMEOUT == driverErrorCode // socket timeout - && (!isDBMirroring || attemptNumber > 0)) // If mirroring, only close after failover has been tried (attempt >= 1) - || timerHasExpired(timerExpire) - // for non-dbmirroring cases, do not retry after tcp socket connection succeeds + if (isFatalError(e) // do not retry on fatal errors + || timerHasExpired(timerExpire) // no time left + || (timerRemaining(timerExpire) < TimeUnit.SECONDS.toMillis(connectRetryInterval) + + 2 * timeForFirstTry) // not enough time for another retry + || (connectRetryCount == 0 && !isDBMirroring && !useTnir) // retries disabled + // retry at least once for TNIR and failover + || (connectRetryCount == 0 && (isDBMirroring || useTnir) && attemptNumber > 0) + || (connectRetryCount != 0 && attemptNumber >= connectRetryCount) // no retries left ) { if (loggerResiliency.isLoggable(Level.FINER)) { - loggerResiliency.finer( - toString() + " Connection open - connection failed on attempt: " + attemptNumber + "."); - loggerResiliency.finer(toString() + " Connection open - connection failure. Driver error code: " - + driverErrorCode); - if (null != sqlServerError && !sqlServerError.getErrorMessage().isEmpty()) { - loggerResiliency - .finer(toString() + " Connection open - connection failure. SQL Server error : " - + sqlServerError.getErrorMessage()); - } + logConnectFailure(attemptNumber, e, sqlServerError); } // close the connection and throw the error back @@ -3469,15 +3475,7 @@ private void login(String primary, String primaryInstanceName, int primaryPortNu throw e; } else { if (loggerResiliency.isLoggable(Level.FINER)) { - loggerResiliency.finer( - toString() + " Connection open - connection failed on attempt: " + attemptNumber + "."); - loggerResiliency.finer(toString() + " Connection open - connection failure. Driver error code: " - + driverErrorCode); - if (null != sqlServerError && !sqlServerError.getErrorMessage().isEmpty()) { - loggerResiliency - .finer(toString() + " Connection open - connection failure. SQL Server error : " - + sqlServerError.getErrorMessage()); - } + logConnectFailure(attemptNumber, e, sqlServerError); } // Close the TDS channel from the failed connection attempt so that we don't @@ -3496,15 +3494,7 @@ private void login(String primary, String primaryInstanceName, int primaryPortNu if (remainingMilliseconds <= fedauthRetryInterval) { if (loggerResiliency.isLoggable(Level.FINER)) { - loggerResiliency.finer(toString() + " Connection open - connection failed on attempt: " - + attemptNumber + "."); - loggerResiliency.finer(toString() - + " Connection open - connection failure. Driver error code: " + driverErrorCode); - if (null != sqlServerError && !sqlServerError.getErrorMessage().isEmpty()) { - loggerResiliency - .finer(toString() + " Connection open - connection failure. SQL Server error : " - + sqlServerError.getErrorMessage()); - } + logConnectFailure(attemptNumber, e, sqlServerError); } throw e; @@ -3517,19 +3507,23 @@ private void login(String primary, String primaryInstanceName, int primaryPortNu // the network with requests, then update sleep interval for next iteration (max 1 second interval) // We have to sleep for every attempt in case of non-dbMirroring scenarios (including multisubnetfailover), // Whereas for dbMirroring, we sleep for every two attempts as each attempt is to a different server. - if (!isDBMirroring || (1 == attemptNumber % 2)) { - if (loggerResiliency.isLoggable(Level.FINER)) { - loggerResiliency.finer(toString() + " Connection open - sleeping milisec: " + connectRetryInterval); - } - if (loggerResiliency.isLoggable(Level.FINER)) { - loggerResiliency.finer(toString() + " Connection open - connection failed on transient error " - + (sqlServerError != null ? sqlServerError.getErrorNumber() : "") - + ". Wait for connectRetryInterval(" + connectRetryInterval + ")s before retry #" - + attemptNumber); - } + // Make sure there's enough time to do another retry + if (!isDBMirroring || (isDBMirroring && (0 == attemptNumber % 2)) + && (attemptNumber < connectRetryCount && connectRetryCount != 0) + && timerRemaining( + timerExpire) > (TimeUnit.SECONDS.toMillis(connectRetryInterval) + 2 * timeForFirstTry)) { + + // don't wait for TNIR + if (!(useTnir && attemptNumber == 0)) { + if (loggerResiliency.isLoggable(Level.FINER)) { + loggerResiliency.finer(toString() + " Connection open - connection failed on transient error " + + (sqlServerError != null ? sqlServerError.getErrorNumber() : "") + + ". Wait for connectRetryInterval(" + connectRetryInterval + ")s before retry #" + + attemptNumber); + } - sleepForInterval(fedauthRetryInterval); - fedauthRetryInterval = (fedauthRetryInterval < 500) ? fedauthRetryInterval * 2 : 1000; + sleepForInterval(TimeUnit.SECONDS.toMillis(connectRetryInterval)); + } } // Update timeout interval (but no more than the point where we're supposed to fail: timerExpire) @@ -3622,31 +3616,22 @@ private void login(String primary, String primaryInstanceName, int primaryPortNu } } + // non recoverable or retryable fatal errors boolean isFatalError(SQLServerException e) { /* * NOTE: If these conditions are modified, consider modification to conditions in SQLServerConnection::login() * and Reconnect::run() */ + int errorCode = e.getErrorCode(); + int driverErrorCode = e.getDriverErrorCode(); - // actual logon failed (e.g. bad password) - if ((SQLServerException.LOGON_FAILED == e.getErrorCode()) - // actual logon failed (e.g. password expired) - || (SQLServerException.PASSWORD_EXPIRED == e.getErrorCode()) - // actual logon failed (e.g. user account locked) - || (SQLServerException.USER_ACCOUNT_LOCKED == e.getErrorCode()) - // invalid TDS received from server - || (SQLServerException.DRIVER_ERROR_INVALID_TDS == e.getDriverErrorCode()) - // failure negotiating SSL - || (SQLServerException.DRIVER_ERROR_SSL_FAILED == e.getDriverErrorCode()) - // failure TLS1.2 - || (SQLServerException.DRIVER_ERROR_INTERMITTENT_TLS_FAILED == e.getDriverErrorCode()) - // unsupported configuration (e.g. Sphinx, invalid packet size, etc.) - || (SQLServerException.DRIVER_ERROR_UNSUPPORTED_CONFIG == e.getDriverErrorCode()) - // no more time to try again - || (SQLServerException.ERROR_SOCKET_TIMEOUT == e.getDriverErrorCode())) - return true; - else - return false; + return ((SQLServerException.LOGON_FAILED == errorCode) // logon failed (eg bad password) + || (SQLServerException.PASSWORD_EXPIRED == errorCode) // password expired + || (SQLServerException.USER_ACCOUNT_LOCKED == errorCode) // user account locked + || (SQLServerException.DRIVER_ERROR_INVALID_TDS == driverErrorCode) // invalid TDS from server + || (SQLServerException.DRIVER_ERROR_SSL_FAILED == driverErrorCode) // SSL failure + || (SQLServerException.DRIVER_ERROR_INTERMITTENT_TLS_FAILED == driverErrorCode) // TLS1.2 failure + || (SQLServerException.DRIVER_ERROR_UNSUPPORTED_CONFIG == driverErrorCode)); // unsupported config (eg Sphinx, invalid packet size ,etc) } // reset all params that could have been changed due to ENVCHANGE tokens to defaults, @@ -3707,7 +3692,7 @@ static boolean timerHasExpired(long timerExpire) { } /** - * Get time remaining to timer expiry + * Get time remaining to timer expiry (in ms) * * @param timerExpire * @return remaining time to expiry @@ -5979,7 +5964,7 @@ private SqlAuthenticationToken getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throw String user = activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString()); // No of milliseconds to sleep for the initial back off. - int sleepInterval = BACKOFF_INTERVAL; + int fedauthSleepInterval = BACKOFF_INTERVAL; if (!msalContextExists() && !authenticationString.equalsIgnoreCase(SqlAuthentication.ACTIVE_DIRECTORY_INTEGRATED.toString())) { @@ -6078,7 +6063,7 @@ private SqlAuthenticationToken getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throw int millisecondsRemaining = timerRemaining(timerExpire); if (ActiveDirectoryAuthentication.GET_ACCESS_TOKEN_TRANSIENT_ERROR != errorCategory - || timerHasExpired(timerExpire) || (sleepInterval >= millisecondsRemaining)) { + || timerHasExpired(timerExpire) || (fedauthSleepInterval >= millisecondsRemaining)) { String errorStatus = Integer.toHexString(adalException.getStatus()); @@ -6102,13 +6087,14 @@ private SqlAuthenticationToken getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throw if (connectionlogger.isLoggable(Level.FINER)) { connectionlogger.fine(toString() + " SQLServerConnection.getFedAuthToken sleeping: " - + sleepInterval + " milliseconds."); + + fedauthSleepInterval + " milliseconds."); connectionlogger.fine(toString() + " SQLServerConnection.getFedAuthToken remaining: " + millisecondsRemaining + " milliseconds."); } - sleepForInterval(sleepInterval); - sleepInterval = sleepInterval * 2; + sleepForInterval(fedauthSleepInterval); + fedauthSleepInterval = (fedauthSleepInterval < 500) ? fedauthSleepInterval * 2 : 1000; + } } // else choose MSAL4J for integrated authentication. This option is supported for both windows and unix, diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java index 7d4fa23f0..943804dcf 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java @@ -395,16 +395,17 @@ static String generateStateCode(SQLServerConnection con, int errNum, Integer dat static String checkAndAppendClientConnId(String errMsg, SQLServerConnection conn) { if (null != conn && conn.isConnected()) { UUID clientConnId = conn.getClientConIdInternal(); - assert null != clientConnId; - StringBuilder sb = new StringBuilder(errMsg); - // This syntax of adding connection id is matched in a retry logic. If anything changes here, make - // necessary changes to enableSSL() function's exception handling mechanism. - sb.append(LOG_CLIENT_CONNECTION_ID_PREFIX); - sb.append(clientConnId.toString()); - return sb.toString(); - } else { - return errMsg; + if (null != clientConnId) { + StringBuilder sb = (errMsg != null) ? new StringBuilder(errMsg) : new StringBuilder(); + // This syntax of adding connection id is matched in a retry logic. If anything changes here, make + // necessary changes to enableSSL() function's exception handling mechanism. + sb.append(LOG_CLIENT_CONNECTION_ID_PREFIX); + sb.append(clientConnId.toString()); + return sb.toString(); + } } + return (errMsg != null) ? errMsg : ""; + } static void throwNotSupportedException(SQLServerConnection con, Object obj) throws SQLServerException { diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java index eabbbb574..084a2ae9d 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java @@ -53,6 +53,7 @@ public class SQLServerConnectionTest extends AbstractTest { // If no retry is done, the function should at least exit in 5 seconds static int threshHoldForNoRetryInMilliseconds = 5000; static int loginTimeOutInSeconds = 10; + static String tnirHost = getConfiguredProperty("tnirHost"); String randomServer = RandomUtil.getIdentifier("Server"); @@ -489,6 +490,44 @@ public void testConnectCountInLoginAndCorrectRetryCount() { } } + // Test connect retry 0 but should still connect to TNIR + @Test + @Tag(Constants.xAzureSQLDW) + @Tag(Constants.xAzureSQLDB) + @Tag(Constants.reqExternalSetup) + public void testConnectTnir() { + org.junit.Assume.assumeTrue(isWindows); + + // no retries but should connect to TNIR (this assumes host is defined in host file + try (Connection con = PrepUtil + .getConnection(connectionString + ";transparentNetworkIPResolution=true;connectRetryCount=0;serverName=" + + tnirHost);) {} catch (Exception e) { + fail(e.getMessage()); + } + } + + // Test connect retry 0 and TNIR disabled + @Test + @Tag(Constants.xAzureSQLDW) + @Tag(Constants.xAzureSQLDB) + @Tag(Constants.reqExternalSetup) + public void testConnectNoTnir() { + org.junit.Assume.assumeTrue(isWindows); + + // no retries no TNIR should fail even tho host is defined in host file + try (Connection con = PrepUtil.getConnection( + connectionString + ";transparentNetworkIPResolution=false;connectRetryCount=0;serverName=" + tnirHost);) { + assertTrue(con == null, TestResource.getResource("R_shouldNotConnect")); + } catch (Exception e) { + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_tcpipConnectionFailed")) + || ((isSqlAzure() || isSqlAzureDW()) + ? e.getMessage().contains( + TestResource.getResource("R_connectTimedOut")) + : false), + e.getMessage()); + } + } + @Test @Tag(Constants.xAzureSQLDW) @Tag(Constants.xAzureSQLDB) @@ -981,7 +1020,9 @@ public void run() { ds.setURL(connectionString); ds.setServerName("invalidServerName" + UUID.randomUUID()); - ds.setLoginTimeout(5); + ds.setLoginTimeout(30); + ds.setConnectRetryCount(3); + ds.setConnectRetryInterval(10); try (Connection con = ds.getConnection()) {} catch (SQLException e) {} } }; diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java index af2bda8af..68fbfc8df 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java @@ -9,7 +9,6 @@ import static org.junit.jupiter.api.Assertions.fail; import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; @@ -36,67 +35,81 @@ @RunWith(JUnitPlatform.class) -@Tag("slow") public class TimeoutTest extends AbstractTest { static String randomServer = RandomUtil.getIdentifier("Server"); static String waitForDelaySPName = RandomUtil.getIdentifier("waitForDelaySP"); static final int waitForDelaySeconds = 10; - static final int defaultTimeout = 15; // loginTimeout default value + static final int defaultTimeout = 30; // loginTimeout default value @BeforeAll public static void setupTests() throws Exception { setConnection(); } + /* + * TODO: + * The tests below uses a simple interval counting logic to determine whether there was at least 1 retry. + * Given the interval is long enough, then 1 retry should take at least 1 interval long, so if it took < 1 interval, then it assumes there were no retry. However, this only works if TNIR or failover is not enabled since those cases should retry but no wait interval in between. So this interval counting can not detect these cases. + * Note a better and more reliable way would be to check attemptNumber using reflection to determine the number of retries. + */ + + // test default loginTimeout used if not specified in connection string @Test public void testDefaultLoginTimeout() { - long timerEnd = 0; - + long totalTime = 0; long timerStart = System.currentTimeMillis(); - // Try a non existing server and see if the default timeout is 15 seconds - try (Connection con = PrepUtil.getConnection("jdbc:sqlserver://" + randomServer + "connectRetryCount=0")) { + + // non existing server and default values to see if took default timeout + try (Connection con = PrepUtil.getConnection("jdbc:sqlserver://" + randomServer)) { fail(TestResource.getResource("R_shouldNotConnect")); } catch (Exception e) { - timerEnd = System.currentTimeMillis(); + totalTime = System.currentTimeMillis() - timerStart; - assertTrue((e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost"))) - || ((isSqlAzure() || isSqlAzureDW()) - ? e.getMessage().contains( - TestResource.getResource("R_connectTimedOut")) - : false), + assertTrue( + (e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_tcpipConnectionToHost").toLowerCase())) + || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), e.getMessage()); } - verifyTimeout(timerEnd - timerStart, defaultTimeout); + // time should be < default loginTimeout + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(defaultTimeout), + "total time: " + totalTime + " default loginTimout: " + TimeUnit.SECONDS.toMillis(defaultTimeout)); } + // test setting loginTimeout value @Test public void testURLLoginTimeout() { - long timerEnd = 0; - int timeout = 10; + long totalTime = 0; + int timeout = 15; long timerStart = System.currentTimeMillis(); + // non existing server and set loginTimeout try (Connection con = PrepUtil.getConnection("jdbc:sqlserver://" + randomServer + ";logintimeout=" + timeout)) { fail(TestResource.getResource("R_shouldNotConnect")); } catch (Exception e) { - timerEnd = System.currentTimeMillis(); + totalTime = System.currentTimeMillis() - timerStart; - assertTrue((e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost"))) - || ((isSqlAzure() || isSqlAzureDW()) - ? e.getMessage().contains( - TestResource.getResource("R_connectTimedOut")) - : false), + assertTrue( + (e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_tcpipConnectionToHost").toLowerCase())) + || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), e.getMessage()); } - verifyTimeout(timerEnd - timerStart, timeout); + // time should be < set loginTimeout + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(timeout), + "total time: " + totalTime + " loginTimeout: " + TimeUnit.SECONDS.toMillis(timeout)); } + // test setting timeout in DM @Test public void testDMLoginTimeoutApplied() { - long timerEnd = 0; - int timeout = 10; + long totalTime = 0; + int timeout = 15; DriverManager.setLoginTimeout(timeout); long timerStart = System.currentTimeMillis(); @@ -104,23 +117,26 @@ public void testDMLoginTimeoutApplied() { try (Connection con = PrepUtil.getConnection("jdbc:sqlserver://" + randomServer)) { fail(TestResource.getResource("R_shouldNotConnect")); } catch (Exception e) { - timerEnd = System.currentTimeMillis(); + totalTime = System.currentTimeMillis() - timerStart; - assertTrue((e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost"))) - || ((isSqlAzure() || isSqlAzureDW()) - ? e.getMessage().contains( - TestResource.getResource("R_connectTimedOut")) - : false), + assertTrue( + (e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_tcpipConnectionToHost").toLowerCase())) + || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), e.getMessage()); } - verifyTimeout(timerEnd - timerStart, timeout); + // time should be < DM timeout + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(timeout), + "total time: " + totalTime + " DM loginTimeout: " + TimeUnit.SECONDS.toMillis(timeout)); } + // test that setting in connection string overrides value set in DM @Test public void testDMLoginTimeoutNotApplied() { - long timerEnd = 0; - int timeout = 10; + long totalTime = 0; + int timeout = 15; try { DriverManager.setLoginTimeout(timeout * 3); // 30 seconds long timerStart = System.currentTimeMillis(); @@ -129,134 +145,172 @@ public void testDMLoginTimeoutNotApplied() { .getConnection("jdbc:sqlserver://" + randomServer + ";loginTimeout=" + timeout)) { fail(TestResource.getResource("R_shouldNotConnect")); } catch (Exception e) { - timerEnd = System.currentTimeMillis(); + totalTime = System.currentTimeMillis() - timerStart; assertTrue( - (e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost"))) - || ((isSqlAzure() || isSqlAzureDW()) - ? e.getMessage() - .contains(TestResource - .getResource("R_connectTimedOut")) - : false), + (e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_tcpipConnectionToHost").toLowerCase())) + || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), e.getMessage()); } - verifyTimeout(timerEnd - timerStart, timeout); + + // time should be < connection string loginTimeout + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(timeout), + "total time: " + totalTime + " loginTimeout: " + TimeUnit.SECONDS.toMillis(timeout)); } finally { DriverManager.setLoginTimeout(0); // Default to 0 again } } + // Test connect retry set to 0 (disabled) + @Test + public void testConnectRetryDisable() { + long totalTime = 0; + long timerStart = System.currentTimeMillis(); + int interval = defaultTimeout; // long interval so we can tell if there was a retry + long timeout = defaultTimeout * 2; // long loginTimeout to accommodate the long interval + + // non existent server with long loginTimeout, should return fast if no retries at all + try (Connection con = PrepUtil.getConnection( + "jdbc:sqlserver://" + randomServer + ";transparentNetworkIPResolution=false;loginTimeout=" + timeout + + ";connectRetryCount=0;connectInterval=" + interval)) { + fail(TestResource.getResource("R_shouldNotConnect")); + } catch (Exception e) { + totalTime = System.currentTimeMillis() - timerStart; + + assertTrue( + e.getMessage().matches(TestUtils.formatErrorMsg("R_tcpipConnectionFailed")) + || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), + e.getMessage()); + } + + // if there was a retry then it would take at least 1 interval long, so if < interval means there were no retries + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(interval), + "total time: " + totalTime + " interval: " + TimeUnit.SECONDS.toMillis(interval)); + } + // Test connect retry for non-existent server with loginTimeout @Test public void testConnectRetryBadServer() { - long timerEnd = 0; + long totalTime = 0; long timerStart = System.currentTimeMillis(); - int loginTimeout = 15; + int timeout = 15; // non existent server with very short loginTimeout, no retry will happen as not a transient error - try (Connection con = PrepUtil - .getConnection("jdbc:sqlserver://" + randomServer + ";loginTimeout=" + loginTimeout)) { + try (Connection con = PrepUtil.getConnection("jdbc:sqlserver://" + randomServer + ";loginTimeout=" + timeout)) { fail(TestResource.getResource("R_shouldNotConnect")); } catch (Exception e) { - timerEnd = System.currentTimeMillis(); + totalTime = System.currentTimeMillis() - timerStart; - assertTrue((e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost"))) - || ((isSqlAzure() || isSqlAzureDW()) - ? e.getMessage().contains( - TestResource.getResource("R_connectTimedOut")) - : false), + assertTrue( + (e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_tcpipConnectionToHost").toLowerCase())) + || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), e.getMessage()); } - verifyTimeout(timerEnd - timerStart, loginTimeout); + // time should be < loginTimeout set + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(timeout), + "total time: " + totalTime + " loginTimeout: " + TimeUnit.SECONDS.toMillis(timeout)); } // Test connect retry for database error @Test public void testConnectRetryServerError() { - long timerEnd = 0; + long totalTime = 0; long timerStart = System.currentTimeMillis(); + int interval = defaultTimeout; // long interval so we can tell if there was a retry + long timeout = defaultTimeout * 2; // long loginTimeout to accommodate the long interval - // non existent database with interval < loginTimeout this will generate a 4060 transient error and retry - int connectRetryCount = new Random().nextInt(256); - int connectRetryInterval = new Random().nextInt(defaultTimeout) + 1; + // non existent database with interval < loginTimeout this will generate a 4060 transient error and retry 1 time try (Connection con = PrepUtil.getConnection( TestUtils.addOrOverrideProperty(connectionString, "database", RandomUtil.getIdentifier("database")) - + ";logintimeout=" + defaultTimeout + ";connectRetryCount=" + connectRetryCount - + ";connectRetryInterval=" + connectRetryInterval)) { + + ";loginTimeout=" + timeout + ";connectRetryCount=" + 1 + ";connectRetryInterval=" + interval + + ";transparentNetworkIPResolution=false")) { fail(TestResource.getResource("R_shouldNotConnect")); } catch (Exception e) { - timerEnd = System.currentTimeMillis(); + totalTime = System.currentTimeMillis() - timerStart; - assertTrue((e.getMessage().contains(TestResource.getResource("R_cannotOpenDatabase"))) - || ((isSqlAzure() || isSqlAzureDW()) - ? e.getMessage().contains( - TestResource.getResource("R_connectTimedOut")) - : false), + assertTrue( + (e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) + || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), e.getMessage()); } - // connect + all retries should always be <= loginTimeout - verifyTimeout(timerEnd - timerStart, defaultTimeout); + // 1 retry should be at least 1 interval long but < 2 intervals + assertTrue(TimeUnit.SECONDS.toMillis(interval) < totalTime, + "interval: " + TimeUnit.SECONDS.toMillis(interval) + " total time: " + totalTime); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(2 * interval), + "total time: " + totalTime + " 2 * interval: " + TimeUnit.SECONDS.toMillis(interval)); } // Test connect retry for database error using Datasource @Test public void testConnectRetryServerErrorDS() { - long timerEnd = 0; + long totalTime = 0; long timerStart = System.currentTimeMillis(); + int interval = defaultTimeout; // long interval so we can tell if there was a retry + long loginTimeout = defaultTimeout * 2; // long loginTimeout to accommodate the long interval - // non existent database with interval < loginTimeout this will generate a 4060 transient error and retry - int connectRetryCount = new Random().nextInt(256); - int connectRetryInterval = new Random().nextInt(defaultTimeout) + 1; - + // non existent database with interval < loginTimeout this will generate a 4060 transient error and retry 1 time SQLServerDataSource ds = new SQLServerDataSource(); String connectStr = TestUtils.addOrOverrideProperty(connectionString, "database", - RandomUtil.getIdentifier("database")) + ";logintimeout=" + defaultTimeout + ";connectRetryCount=" - + connectRetryCount + ";connectRetryInterval=" + connectRetryInterval; + RandomUtil.getIdentifier("database")) + ";logintimeout=" + loginTimeout + ";connectRetryCount=1" + + ";connectRetryInterval=" + interval; updateDataSource(connectStr, ds); try (Connection con = PrepUtil.getConnection(connectStr)) { fail(TestResource.getResource("R_shouldNotConnect")); } catch (Exception e) { - assertTrue((e.getMessage().contains(TestResource.getResource("R_cannotOpenDatabase"))) - || ((isSqlAzure() || isSqlAzureDW()) - ? e.getMessage().contains( - TestResource.getResource("R_connectTimedOut")) - : false), + assertTrue( + (e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) + || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), e.getMessage()); - timerEnd = System.currentTimeMillis(); + totalTime = System.currentTimeMillis() - timerStart; } - // connect + all retries should always be <= loginTimeout - verifyTimeout(timerEnd - timerStart, defaultTimeout); + // 1 retry should be at least 1 interval long but < 2 intervals + assertTrue(TimeUnit.SECONDS.toMillis(interval) < totalTime, + "interval: " + TimeUnit.SECONDS.toMillis(interval) + " total time: " + totalTime); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(2 * interval), + "total time: " + totalTime + " 2 * interval: " + TimeUnit.SECONDS.toMillis(2 * interval)); } // Test connect retry for database error with loginTimeout @Test public void testConnectRetryTimeout() { - long timerEnd = 0; + long totalTime = 0; long timerStart = System.currentTimeMillis(); + int interval = defaultTimeout; // long interval so we can tell if there was a retry int loginTimeout = 2; - // non existent database with very short loginTimeout so there is no time to do all retries + // non existent database with very short loginTimeout so there is no time to do any retry try (Connection con = PrepUtil.getConnection( TestUtils.addOrOverrideProperty(connectionString, "database", RandomUtil.getIdentifier("database")) - + "connectRetryCount=" + (new Random().nextInt(256)) + ";connectRetryInterval=" - + (new Random().nextInt(defaultTimeout - 1) + 1) + ";loginTimeout=" + loginTimeout)) { + + "connectRetryCount=" + (new Random().nextInt(256)) + ";connectRetryInterval=" + interval + + ";loginTimeout=" + loginTimeout)) { fail(TestResource.getResource("R_shouldNotConnect")); } catch (Exception e) { - timerEnd = System.currentTimeMillis(); + totalTime = System.currentTimeMillis() - timerStart; - assertTrue((e.getMessage().contains(TestResource.getResource("R_cannotOpenDatabase"))) - || ((isSqlAzure() || isSqlAzureDW()) - ? e.getMessage().contains( - TestResource.getResource("R_connectTimedOut")) - : false), + assertTrue( + (e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) + || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), e.getMessage()); } - verifyTimeout(timerEnd - timerStart, loginTimeout); + // if there was a retry then it would take at least 1 interval long, so if < interval means there were no retries + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(interval), + "total time: " + totalTime + " interval: " + TimeUnit.SECONDS.toMillis(interval)); } // Test for detecting Azure server for connection retries @@ -289,80 +343,6 @@ public void testAzureEndpointRetry() { } } - @Test - public void testFailoverInstanceResolution() throws SQLException { - long timerEnd = 0; - long timerStart = System.currentTimeMillis(); - - // Try a non existing server and see if the default timeout is 15 seconds - try (Connection con = PrepUtil - .getConnection("jdbc:sqlserver://" + randomServer + ";databaseName=FailoverDB_abc;failoverPartner=" - + randomServer + "\\foo;user=sa;password=" + RandomUtil.getIdentifier("password"))) { - fail(TestResource.getResource("R_shouldNotConnect")); - } catch (Exception e) { - timerEnd = System.currentTimeMillis(); - - assertTrue((e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost"))) - || ((isSqlAzure() || isSqlAzureDW()) - ? e.getMessage().contains( - TestResource.getResource("R_connectTimedOut")) - : false), - e.getMessage()); - } - - verifyTimeout(timerEnd - timerStart, defaultTimeout * 2); - } - - @Test - public void testFOInstanceResolution2() throws SQLException { - long timerEnd = 0; - - long timerStart = System.currentTimeMillis(); - try (Connection con = PrepUtil - .getConnection("jdbc:sqlserver://" + randomServer + "\\fooggg;databaseName=FailoverDB;failoverPartner=" - + randomServer + "\\foo;user=sa;password=" + RandomUtil.getIdentifier("password"))) { - fail(TestResource.getResource("R_shouldNotConnect")); - } catch (Exception e) { - timerEnd = System.currentTimeMillis(); - } - - verifyTimeout(timerEnd - timerStart, defaultTimeout); - } - - /** - * Tests that failover is correctly used after a socket timeout, by confirming total time includes socketTimeout - * for both primary and failover server. - */ - @Test - public void testFailoverInstanceResolutionWithSocketTimeout() { - long timerEnd; - long timerStart = System.currentTimeMillis(); - - try (Connection con = PrepUtil.getConnection("jdbc:sqlserver://" + randomServer - + ";databaseName=FailoverDB;failoverPartner=" + randomServer + "\\foo;user=sa;password=" - + RandomUtil.getIdentifier("password") + ";socketTimeout=" + waitForDelaySeconds)) { - fail(TestResource.getResource("R_shouldNotConnect")); - } catch (Exception e) { - timerEnd = System.currentTimeMillis(); - if (!(e instanceof SQLException)) { - fail(TestResource.getResource("R_unexpectedErrorMessage") + e.getMessage()); - } - - // Driver should correctly attempt to connect to db, experience a socketTimeout, attempt to connect to - // failover, and then have another socketTimeout. So, expected total time is 2 x socketTimeout. - long totalTime = timerEnd - timerStart; - long totalExpectedTime = waitForDelaySeconds * 1000L * 2; // We expect 2 * socketTimeout - assertTrue(totalTime >= totalExpectedTime, TestResource.getResource("R_executionNotLong") + "totalTime: " - + totalTime + " expectedTime: " + totalExpectedTime); - } - } - - private void verifyTimeout(long timeDiff, int timeout) { - // Verify that login timeout does not take longer than seconds. - assertTrue(timeDiff < TimeUnit.SECONDS.toMillis(timeout * 2), - "timeout: " + TimeUnit.SECONDS.toMillis(timeout) + " timediff: " + timeDiff); - } - /** * When query timeout occurs, the connection is still usable. * From eae6d7b33571c92029169b33378b2405e8df448a Mon Sep 17 00:00:00 2001 From: barryw-mssql <135760367+barryw-mssql@users.noreply.github.com> Date: Mon, 4 Mar 2024 16:07:09 -0800 Subject: [PATCH 18/47] Fix to ensure metadata returned follows JDBC data type specs (#2326) --- .../jdbc/SQLServerDatabaseMetaData.java | 93 ++++++++++++++-- .../sqlserver/jdbc/SQLServerResource.java | 3 +- .../sqlserver/jdbc/TestResource.java | 3 +- .../DatabaseMetaDataTest.java | 104 ++++++++++++++++++ 4 files changed, 189 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java index 1d2868879..2dfb40d89 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java @@ -258,11 +258,17 @@ private void checkClosed() throws SQLServerException { private static final String IS_AUTOINCREMENT = "IS_AUTOINCREMENT"; private static final String ACTIVITY_ID = " ActivityId: "; + private static final String NVARCHAR = JDBCType.NVARCHAR.name(); + private static final String VARCHAR = JDBCType.VARCHAR.name(); + private static final String INTEGER = JDBCType.INTEGER.name(); + private static final String SMALLINT = JDBCType.SMALLINT.name(); + private static final String SQL_KEYWORDS = createSqlKeyWords(); // Use LinkedHashMap to force retrieve elements in order they were inserted /** getColumns columns */ private LinkedHashMap getColumnsDWColumns = null; + private LinkedHashMap getTypesDWColumns = null; /** getImportedKeys columns */ private volatile LinkedHashMap getImportedKeysDWColumns; private static final Lock LOCK = new ReentrantLock(); @@ -630,10 +636,13 @@ public java.sql.ResultSet getColumns(String catalog, String schema, String table + "INSERT INTO @mssqljdbc_temp_sp_columns_result EXEC sp_columns_100 ?,?,?,?,?,?;" - + "SELECT TABLE_QUALIFIER AS TABLE_CAT, TABLE_OWNER AS TABLE_SCHEM, TABLE_NAME, COLUMN_NAME, DATA_TYPE," - + "TYPE_NAME, PRECISION AS COLUMN_SIZE, LENGTH AS BUFFER_LENGTH, SCALE AS DECIMAL_DIGITS, RADIX AS NUM_PREC_RADIX," - + "NULLABLE, REMARKS, COLUMN_DEF, SQL_DATA_TYPE, SQL_DATETIME_SUB, CHAR_OCTET_LENGTH, ORDINAL_POSITION, IS_NULLABLE," - + "NULL AS SCOPE_CATALOG, NULL AS SCOPE_SCHEMA, NULL AS SCOPE_TABLE, SS_DATA_TYPE AS SOURCE_DATA_TYPE," + + "SELECT TABLE_QUALIFIER AS TABLE_CAT, TABLE_OWNER AS TABLE_SCHEM, TABLE_NAME, COLUMN_NAME, " + + "CAST(DATA_TYPE AS INT) AS DATA_TYPE,TYPE_NAME, PRECISION AS COLUMN_SIZE, LENGTH AS BUFFER_LENGTH, " + + "CAST(SCALE AS INT) AS DECIMAL_DIGITS, CAST(RADIX AS INT) AS NUM_PREC_RADIX,CAST(NULLABLE AS INT) AS NULLABLE, " + + "CAST(REMARKS AS VARCHAR) AS REMARKS, COLUMN_DEF, CAST(SQL_DATA_TYPE AS INT) AS SQL_DATA_TYPE, " + + "CAST(SQL_DATETIME_SUB AS INT) AS SQL_DATETIME_SUB, CHAR_OCTET_LENGTH, ORDINAL_POSITION, IS_NULLABLE," + + "CAST(NULL AS VARCHAR) AS SCOPE_CATALOG, CAST(NULL AS VARCHAR) AS SCOPE_SCHEMA, CAST(NULL AS VARCHAR) AS SCOPE_TABLE, " + + "CAST(SS_DATA_TYPE AS SMALLINT) AS SOURCE_DATA_TYPE, " + "CASE SS_IS_IDENTITY WHEN 0 THEN 'NO' WHEN 1 THEN 'YES' WHEN '' THEN '' END AS IS_AUTOINCREMENT," + "CASE SS_IS_COMPUTED WHEN 0 THEN 'NO' WHEN 1 THEN 'YES' WHEN '' THEN '' END AS IS_GENERATEDCOLUMN, " + "SS_IS_SPARSE, SS_IS_COLUMN_SET, SS_UDT_CATALOG_NAME, SS_UDT_SCHEMA_NAME, SS_UDT_ASSEMBLY_TYPE_NAME," @@ -721,6 +730,53 @@ public java.sql.ResultSet getColumns(String catalog, String schema, String table getColumnsDWColumns.put(27, SS_XML_SCHEMACOLLECTION_SCHEMA_NAME); getColumnsDWColumns.put(28, SS_XML_SCHEMACOLLECTION_NAME); } + if (null == getTypesDWColumns) { + getTypesDWColumns = new LinkedHashMap<>(); + getTypesDWColumns.put(1, NVARCHAR); // TABLE_CAT + getTypesDWColumns.put(2, NVARCHAR); // TABLE_SCHEM + getTypesDWColumns.put(3, NVARCHAR); // TABLE_NAME + getTypesDWColumns.put(4, NVARCHAR); // COLUMN_NAME + getTypesDWColumns.put(5, INTEGER); // DATA_TYPE + getTypesDWColumns.put(6, NVARCHAR); // TYPE_NAME + getTypesDWColumns.put(7, INTEGER); // COLUMN_SIZE + getTypesDWColumns.put(8, INTEGER); // BUFFER_LENGTH + getTypesDWColumns.put(9, INTEGER); // DECIMAL_DIGITS + getTypesDWColumns.put(10, INTEGER); // NUM_PREC_RADIX + getTypesDWColumns.put(11, INTEGER); // NULLABLE + getTypesDWColumns.put(12, VARCHAR); // REMARKS + getTypesDWColumns.put(13, NVARCHAR); // COLUMN_DEF + getTypesDWColumns.put(14, INTEGER); // SQL_DATA_TYPE + getTypesDWColumns.put(15, INTEGER); // SQL_DATETIME_SUB + getTypesDWColumns.put(16, INTEGER); // CHAR_OCTET_LENGTH + getTypesDWColumns.put(17, INTEGER); // ORDINAL_POSITION + getTypesDWColumns.put(18, VARCHAR); // IS_NULLABLE + /* + * Use negative value keys to indicate that this column doesn't exist in SQL Server and should just + * be queried as 'NULL' + */ + getTypesDWColumns.put(-1, VARCHAR); // SCOPE_CATALOG + getTypesDWColumns.put(-2, VARCHAR); // SCOPE_SCHEMA + getTypesDWColumns.put(-3, VARCHAR); // SCOPE_TABLE + getTypesDWColumns.put(29, SMALLINT); // SOURCE_DATA_TYPE + getTypesDWColumns.put(22, VARCHAR); // IS_AUTOINCREMENT + getTypesDWColumns.put(21, VARCHAR); // IS_GENERATEDCOLUMN + getTypesDWColumns.put(19, SMALLINT); // SS_IS_SPARSE + getTypesDWColumns.put(20, SMALLINT); // SS_IS_COLUMN_SET + getTypesDWColumns.put(23, NVARCHAR); // SS_UDT_CATALOG_NAME + getTypesDWColumns.put(24, NVARCHAR); // SS_UDT_SCHEMA_NAME + getTypesDWColumns.put(25, NVARCHAR); // SS_UDT_ASSEMBLY_TYPE_NAME + getTypesDWColumns.put(26, NVARCHAR); // SS_XML_SCHEMACOLLECTION_CATALOG_NAME + getTypesDWColumns.put(27, NVARCHAR); // SS_XML_SCHEMACOLLECTION_SCHEMA_NAME + getTypesDWColumns.put(28, NVARCHAR); // SS_XML_SCHEMACOLLECTION_NAME + } + + // Ensure there is a data type for every metadata column + if (getColumnsDWColumns.size() != getTypesDWColumns.size()) { + MessageFormat form = new MessageFormat( + SQLServerException.getErrString("R_colCountNotMatchColTypeCount")); + Object[] msgArgs = {getColumnsDWColumns.size(), getTypesDWColumns.size()}; + throw new IllegalArgumentException(form.format(msgArgs)); + } } finally { LOCK.unlock(); } @@ -744,7 +800,7 @@ public java.sql.ResultSet getColumns(String catalog, String schema, String table if (!isFirstRow) { azureDwSelectBuilder.append(" UNION ALL "); } - azureDwSelectBuilder.append(generateAzureDWSelect(rs, getColumnsDWColumns)); + azureDwSelectBuilder.append(generateAzureDWSelect(rs, getColumnsDWColumns, getTypesDWColumns)); isFirstRow = false; } @@ -780,28 +836,41 @@ public java.sql.ResultSet getColumns(String catalog, String schema, String table } } - private String generateAzureDWSelect(ResultSet rs, Map columns) throws SQLException { + private String generateAzureDWSelect(ResultSet rs, Map columns, + Map types) throws SQLException { StringBuilder sb = new StringBuilder("SELECT "); + for (Entry p : columns.entrySet()) { + String dataType = types.get(p.getKey()); + + // Verify there is a valid column entry in the Data Type lookup map + if (dataType == null) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidArgument")); + Object[] msgArgs = {p.getKey()}; + throw new SQLServerException(null, form.format(msgArgs), null, 0, true); + } + + sb.append("CAST("); if (p.getKey() < 0) { - sb.append("NULL"); + sb.append("NULL AS " + dataType); } else { Object o = rs.getObject(p.getKey()); if (null == o) { - sb.append("NULL"); + sb.append("NULL AS " + dataType); } else if (o instanceof Number) { if (IS_AUTOINCREMENT.equalsIgnoreCase(p.getValue()) || IS_GENERATEDCOLUMN.equalsIgnoreCase(p.getValue())) { sb.append("'").append(Util.escapeSingleQuotes(Util.zeroOneToYesNo(((Number) o).intValue()))) - .append("'"); + .append("' AS ").append(dataType); } else { - sb.append(o.toString()); + sb.append(o.toString()).append(" AS ").append(dataType); } } else { - sb.append("'").append(Util.escapeSingleQuotes(o.toString())).append("'"); + sb.append("'").append(Util.escapeSingleQuotes(o.toString())).append("' AS ").append(dataType) + .append("(").append(Integer.toString(o.toString().length())).append(")"); } } - sb.append(" AS ").append(p.getValue()).append(","); + sb.append(") AS ").append(p.getValue()).append(","); } sb.setLength(sb.length() - 1); return sb.toString(); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index 95b5ffa0b..6cd502b72 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -545,7 +545,8 @@ protected Object[][] getContents() { {"R_ManagedIdentityTokenAcquisitionFail", "Failed to acquire managed identity token. Request for the token succeeded, but no token was returned. The token is null."}, {"R_AmbiguousRowUpdate", "Failed to execute updateRow(). The update is attempting an ambiguous update on tables \"{0}\" and \"{1}\". Ensure all columns being updated prior to the updateRow() call belong to the same table."}, {"R_InvalidSqlQuery", "Invalid SQL Query: {0}"}, - {"R_InvalidScale", "Scale of input value is larger than the maximum allowed by SQL Server."} + {"R_InvalidScale", "Scale of input value is larger than the maximum allowed by SQL Server."}, + {"R_colCountNotMatchColTypeCount", "Number of provided columns {0} does not match the column data types definition {1}."}, }; } // @formatter:on diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java index e14bb671a..607254030 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java @@ -212,5 +212,6 @@ protected Object[][] getContents() { {"R_failedFedauth", "Failed to acquire fedauth token: "}, {"R_noLoginModulesConfiguredForJdbcDriver", "javax.security.auth.login.LoginException (No LoginModules configured for SQLJDBCDriver)"}, - {"R_unexpectedThreadCount", "Thread count is higher than expected."}}; + {"R_unexpectedThreadCount", "Thread count is higher than expected."}, + {"R_expectedClassDoesNotMatchActualClass", "Expected column class {0} does not match actual column class {1} for column {2}."}}; } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java index ff2f2fc73..999ccdc1e 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java @@ -63,6 +63,38 @@ public class DatabaseMetaDataTest extends AbstractTest { private static final String functionName = RandomUtil.getIdentifier("DBMetadataFunction"); private static Map getColumnsDWColumns = null; private static Map getImportedKeysDWColumns = null; + private static final String TABLE_CAT = "TABLE_CAT"; + private static final String TABLE_SCHEM = "TABLE_SCHEM"; + private static final String TABLE_NAME = "TABLE_NAME"; + private static final String COLUMN_NAME = "COLUMN_NAME"; + private static final String DATA_TYPE = "DATA_TYPE"; + private static final String TYPE_NAME = "TYPE_NAME"; + private static final String COLUMN_SIZE = "COLUMN_SIZE"; + private static final String BUFFER_LENGTH = "BUFFER_LENGTH"; + private static final String DECIMAL_DIGITS = "DECIMAL_DIGITS"; + private static final String NUM_PREC_RADIX = "NUM_PREC_RADIX"; + private static final String NULLABLE = "NULLABLE"; + private static final String REMARKS = "REMARKS"; + private static final String COLUMN_DEF = "COLUMN_DEF"; + private static final String SQL_DATA_TYPE = "SQL_DATA_TYPE"; + private static final String SQL_DATETIME_SUB = "SQL_DATETIME_SUB"; + private static final String CHAR_OCTET_LENGTH = "CHAR_OCTET_LENGTH"; + private static final String ORDINAL_POSITION = "ORDINAL_POSITION"; + private static final String IS_NULLABLE = "IS_NULLABLE"; + private static final String SCOPE_CATALOG = "SCOPE_CATALOG"; + private static final String SCOPE_SCHEMA = "SCOPE_SCHEMA"; + private static final String SCOPE_TABLE = "SCOPE_TABLE"; + private static final String SOURCE_DATA_TYPE = "SOURCE_DATA_TYPE"; + private static final String IS_AUTOINCREMENT = "IS_AUTOINCREMENT"; + private static final String IS_GENERATEDCOLUMN = "IS_GENERATEDCOLUMN"; + private static final String SS_IS_SPARSE = "SS_IS_SPARSE"; + private static final String SS_IS_COLUMN_SET = "SS_IS_COLUMN_SET"; + private static final String SS_UDT_CATALOG_NAME = "SS_UDT_CATALOG_NAME"; + private static final String SS_UDT_SCHEMA_NAME = "SS_UDT_SCHEMA_NAME"; + private static final String SS_UDT_ASSEMBLY_TYPE_NAME = "SS_UDT_ASSEMBLY_TYPE_NAME"; + private static final String SS_XML_SCHEMACOLLECTION_CATALOG_NAME = "SS_XML_SCHEMACOLLECTION_CATALOG_NAME"; + private static final String SS_XML_SCHEMACOLLECTION_SCHEMA_NAME = "SS_XML_SCHEMACOLLECTION_SCHEMA_NAME"; + private static final String SS_XML_SCHEMACOLLECTION_NAME = "SS_XML_SCHEMACOLLECTION_NAME"; /** * Verify DatabaseMetaData#isWrapperFor and DatabaseMetaData#unwrap. @@ -887,6 +919,78 @@ public void testGetImportedKeysDW() throws SQLException { } } + /** + * Validates the metadata data types defined by JDBC spec. + * Refer to DatabaseMetadata getColumns() specs + * + * @throws SQLException + */ + @Test + public void testValidateColumnMetadata() throws SQLException { + Map> getColumnMetaDataClass = new LinkedHashMap<>(); + + getColumnMetaDataClass.put(TABLE_CAT, String.class); + getColumnMetaDataClass.put(TABLE_SCHEM, String.class); + getColumnMetaDataClass.put(TABLE_NAME, String.class); + getColumnMetaDataClass.put(COLUMN_NAME, String.class); + getColumnMetaDataClass.put(DATA_TYPE, Integer.class); + getColumnMetaDataClass.put(TYPE_NAME, String.class); + getColumnMetaDataClass.put(COLUMN_SIZE, Integer.class); + getColumnMetaDataClass.put(BUFFER_LENGTH, Integer.class); // Not used + getColumnMetaDataClass.put(DECIMAL_DIGITS, Integer.class); + getColumnMetaDataClass.put(NUM_PREC_RADIX, Integer.class); + getColumnMetaDataClass.put(NULLABLE, Integer.class); + getColumnMetaDataClass.put(REMARKS, String.class); + getColumnMetaDataClass.put(COLUMN_DEF, String.class); + getColumnMetaDataClass.put(SQL_DATA_TYPE, Integer.class); + getColumnMetaDataClass.put(SQL_DATETIME_SUB, Integer.class); + getColumnMetaDataClass.put(CHAR_OCTET_LENGTH, Integer.class); + getColumnMetaDataClass.put(ORDINAL_POSITION, Integer.class); + getColumnMetaDataClass.put(IS_NULLABLE, String.class); + getColumnMetaDataClass.put(SCOPE_CATALOG, String.class); + getColumnMetaDataClass.put(SCOPE_SCHEMA, String.class); + getColumnMetaDataClass.put(SCOPE_TABLE, String.class); + getColumnMetaDataClass.put(SOURCE_DATA_TYPE, Short.class); + getColumnMetaDataClass.put(IS_AUTOINCREMENT, String.class); + getColumnMetaDataClass.put(IS_GENERATEDCOLUMN, String.class); + getColumnMetaDataClass.put(SS_IS_SPARSE, Short.class); + getColumnMetaDataClass.put(SS_IS_COLUMN_SET, Short.class); + getColumnMetaDataClass.put(SS_UDT_CATALOG_NAME, String.class); + getColumnMetaDataClass.put(SS_UDT_SCHEMA_NAME, String.class); + getColumnMetaDataClass.put(SS_UDT_ASSEMBLY_TYPE_NAME, String.class); + getColumnMetaDataClass.put(SS_XML_SCHEMACOLLECTION_CATALOG_NAME, String.class); + getColumnMetaDataClass.put(SS_XML_SCHEMACOLLECTION_SCHEMA_NAME, String.class); + getColumnMetaDataClass.put(SS_XML_SCHEMACOLLECTION_NAME, String.class); + + try (Connection conn = getConnection()) { + ResultSetMetaData metadata = conn.getMetaData().getColumns(null, null, tableName, null).getMetaData(); + + // Ensure that there is an expected class for every column in the metadata result set + assertEquals(metadata.getColumnCount(), getColumnMetaDataClass.size()); + + for (int i = 1; i < metadata.getColumnCount(); i++) { + String columnLabel = metadata.getColumnLabel(i); + String columnClassName = metadata.getColumnClassName(i); + Class expectedClass = getColumnMetaDataClass.get(columnLabel); + + // Ensure the metadata column is in the metadata column class map + if (expectedClass == null) { + MessageFormat form1 = new MessageFormat(TestResource.getResource("R_objectNullOrEmpty")); + Object[] msgArgs1 = {"expected metadata column class for column " + columnLabel}; + fail(form1.format(msgArgs1)); + } + + // Ensure the actual and expected column metadata types match + if (!columnClassName.equals(expectedClass.getName())) { + MessageFormat form1 = new MessageFormat( + TestResource.getResource("R_expectedClassDoesNotMatchActualClass")); + Object[] msgArgs1 = {expectedClass.getName(), columnClassName, columnLabel}; + fail(form1.format(msgArgs1)); + } + } + } + } + @BeforeAll public static void setupTable() throws Exception { setConnection(); From 26803d20232ff35ba075e72198c74e0c6530811e Mon Sep 17 00:00:00 2001 From: Ali Rashid Date: Sat, 16 Mar 2024 01:42:56 +0300 Subject: [PATCH 19/47] Permit constructing a microsoft.sql.DateTimeOffset instance from a java.time.OffsetDateTime value (#2340) --- .../java/microsoft/sql/DateTimeOffset.java | 30 +++++++++++++++++++ .../jdbc/datatypes/DataTypesTest.java | 11 +++++++ 2 files changed, 41 insertions(+) diff --git a/src/main/java/microsoft/sql/DateTimeOffset.java b/src/main/java/microsoft/sql/DateTimeOffset.java index e2fcc8409..f57d74a25 100644 --- a/src/main/java/microsoft/sql/DateTimeOffset.java +++ b/src/main/java/microsoft/sql/DateTimeOffset.java @@ -72,6 +72,22 @@ private DateTimeOffset(java.sql.Timestamp timestamp, int minutesOffset) { assert 0 == this.utcMillis % 1000L : "utcMillis: " + this.utcMillis; } + /** + * Constructs a DateTimeOffset from an existing java.time.OffsetDateTime + * + * @param offsetDateTime + * A java.time.OffsetDateTime value + * @apiNote DateTimeOffset represents values to 100 nanosecond precision. If the java.time.OffsetDateTime instance + * represents a value that is more precise, the value is rounded to the nearest multiple of 100 nanoseconds. Values + * within 50 nanoseconds of the next second are rounded up to the next second. + */ + private DateTimeOffset(java.time.OffsetDateTime offsetDateTime) { + int hundredNanos = ((offsetDateTime.getNano() + 50) / 100); + this.utcMillis = (offsetDateTime.toEpochSecond() * 1000) + (hundredNanos / HUNDRED_NANOS_PER_SECOND * 1000); + this.nanos = 100 * (hundredNanos % HUNDRED_NANOS_PER_SECOND); + this.minutesOffset = offsetDateTime.getOffset().getTotalSeconds() / 60; + } + /** * Converts a java.sql.Timestamp value with an integer offset to the equivalent DateTimeOffset value * @@ -105,6 +121,20 @@ public static DateTimeOffset valueOf(java.sql.Timestamp timestamp, Calendar cale (calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET)) / (60 * 1000)); } + /** + * Directly converts a {@link java.time.OffsetDateTime} value to an equivalent {@link DateTimeOffset} value + * + * @param offsetDateTime + * A java.time.OffsetDateTime value + * @return The DateTimeOffset value of the input java.time.OffsetDateTime + * @apiNote DateTimeOffset represents values to 100 nanosecond precision. If the java.time.OffsetDateTime instance + * represents a value that is more precise, the value is rounded to the nearest multiple of 100 nanoseconds. Values + * within 50 nanoseconds of the next second are rounded up to the next second. + */ + public static DateTimeOffset valueOf(java.time.OffsetDateTime offsetDateTime) { + return new DateTimeOffset(offsetDateTime); + } + /** formatted value */ private String formattedValue = null; 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 350f7cfc3..445d23dd0 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/DataTypesTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/DataTypesTest.java @@ -1935,6 +1935,17 @@ public void testGetLocalDateTimeUnstorable() throws Exception { } } + @Test + public void testDateTimeOffsetValueOfOffsetDateTime() throws Exception { + OffsetDateTime expected = OffsetDateTime.now().withSecond(58).withNano(0); + OffsetDateTime roundUp = expected.withSecond(57).withNano(999999950); + OffsetDateTime roundDown = expected.withSecond(58).withNano(49); + + assertEquals(expected, DateTimeOffset.valueOf(expected).getOffsetDateTime()); + assertEquals(expected, DateTimeOffset.valueOf(roundUp).getOffsetDateTime()); + assertEquals(expected, DateTimeOffset.valueOf(roundDown).getOffsetDateTime()); + } + static LocalDateTime getUnstorableValue() throws Exception { ZoneId systemTimezone = ZoneId.systemDefault(); Instant now = Instant.now(); From 00a25560ba95590eb9ca181286024ba0cf889d07 Mon Sep 17 00:00:00 2001 From: Terry Chow <32403408+tkyc@users.noreply.github.com> Date: Mon, 18 Mar 2024 13:05:23 -0700 Subject: [PATCH 20/47] Formatting and java doc fix (#2350) --- .../sqlserver/jdbc/SQLServerConnection.java | 3 +-- src/main/java/microsoft/sql/DateTimeOffset.java | 12 ++++++------ .../sqlserver/jdbc/SQLServerConnectionTest.java | 4 ++-- .../com/microsoft/sqlserver/jdbc/TestResource.java | 3 ++- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 982cf0e8e..b20b6e7f8 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -3509,8 +3509,7 @@ private void login(String primary, String primaryInstanceName, int primaryPortNu // Whereas for dbMirroring, we sleep for every two attempts as each attempt is to a different server. // Make sure there's enough time to do another retry if (!isDBMirroring || (isDBMirroring && (0 == attemptNumber % 2)) - && (attemptNumber < connectRetryCount && connectRetryCount != 0) - && timerRemaining( + && (attemptNumber < connectRetryCount && connectRetryCount != 0) && timerRemaining( timerExpire) > (TimeUnit.SECONDS.toMillis(connectRetryInterval) + 2 * timeForFirstTry)) { // don't wait for TNIR diff --git a/src/main/java/microsoft/sql/DateTimeOffset.java b/src/main/java/microsoft/sql/DateTimeOffset.java index f57d74a25..bf9e95c7b 100644 --- a/src/main/java/microsoft/sql/DateTimeOffset.java +++ b/src/main/java/microsoft/sql/DateTimeOffset.java @@ -74,12 +74,12 @@ private DateTimeOffset(java.sql.Timestamp timestamp, int minutesOffset) { /** * Constructs a DateTimeOffset from an existing java.time.OffsetDateTime + * DateTimeOffset represents values to 100 nanosecond precision. If the java.time.OffsetDateTime instance + * represents a value that is more precise, the value is rounded to the nearest multiple of 100 nanoseconds. Values + * within 50 nanoseconds of the next second are rounded up to the next second. * * @param offsetDateTime * A java.time.OffsetDateTime value - * @apiNote DateTimeOffset represents values to 100 nanosecond precision. If the java.time.OffsetDateTime instance - * represents a value that is more precise, the value is rounded to the nearest multiple of 100 nanoseconds. Values - * within 50 nanoseconds of the next second are rounded up to the next second. */ private DateTimeOffset(java.time.OffsetDateTime offsetDateTime) { int hundredNanos = ((offsetDateTime.getNano() + 50) / 100); @@ -123,13 +123,13 @@ public static DateTimeOffset valueOf(java.sql.Timestamp timestamp, Calendar cale /** * Directly converts a {@link java.time.OffsetDateTime} value to an equivalent {@link DateTimeOffset} value + * DateTimeOffset represents values to 100 nanosecond precision. If the java.time.OffsetDateTime instance + * represents a value that is more precise, the value is rounded to the nearest multiple of 100 nanoseconds. Values + * within 50 nanoseconds of the next second are rounded up to the next second. * * @param offsetDateTime * A java.time.OffsetDateTime value * @return The DateTimeOffset value of the input java.time.OffsetDateTime - * @apiNote DateTimeOffset represents values to 100 nanosecond precision. If the java.time.OffsetDateTime instance - * represents a value that is more precise, the value is rounded to the nearest multiple of 100 nanoseconds. Values - * within 50 nanoseconds of the next second are rounded up to the next second. */ public static DateTimeOffset valueOf(java.time.OffsetDateTime offsetDateTime) { return new DateTimeOffset(offsetDateTime); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java index 084a2ae9d..12eea46cf 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java @@ -515,8 +515,8 @@ public void testConnectNoTnir() { org.junit.Assume.assumeTrue(isWindows); // no retries no TNIR should fail even tho host is defined in host file - try (Connection con = PrepUtil.getConnection( - connectionString + ";transparentNetworkIPResolution=false;connectRetryCount=0;serverName=" + tnirHost);) { + try (Connection con = PrepUtil.getConnection(connectionString + + ";transparentNetworkIPResolution=false;connectRetryCount=0;serverName=" + tnirHost);) { assertTrue(con == null, TestResource.getResource("R_shouldNotConnect")); } catch (Exception e) { assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_tcpipConnectionFailed")) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java index 607254030..0e5f26a90 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java @@ -213,5 +213,6 @@ protected Object[][] getContents() { {"R_noLoginModulesConfiguredForJdbcDriver", "javax.security.auth.login.LoginException (No LoginModules configured for SQLJDBCDriver)"}, {"R_unexpectedThreadCount", "Thread count is higher than expected."}, - {"R_expectedClassDoesNotMatchActualClass", "Expected column class {0} does not match actual column class {1} for column {2}."}}; + {"R_expectedClassDoesNotMatchActualClass", + "Expected column class {0} does not match actual column class {1} for column {2}."}}; } From 41710d20f0356e11f724882311388b36ceb5d898 Mon Sep 17 00:00:00 2001 From: Terry Chow <32403408+tkyc@users.noreply.github.com> Date: Tue, 19 Mar 2024 15:35:00 -0700 Subject: [PATCH 21/47] Default to RMFAIL instead of RMERR (#2348) * Default to RMFAIL instead of RMERR * Code review comments * Code review comments p2 --- .../sqlserver/jdbc/SQLServerXAResource.java | 82 +------------------ .../com/microsoft/sqlserver/jdbc/Util.java | 19 ----- 2 files changed, 1 insertion(+), 100 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java index 6d82bf836..eb96becbc 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java @@ -5,7 +5,6 @@ package com.microsoft.sqlserver.jdbc; -import java.net.SocketException; import java.sql.CallableStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -25,7 +24,6 @@ import javax.transaction.xa.XAResource; import javax.transaction.xa.Xid; -import com.microsoft.sqlserver.jdbc.SQLServerError.TransientError; /** @@ -754,27 +752,13 @@ private XAReturnValue dtc_XA_interface(int nType, Xid xid, int xaFlags) throws X } } } - } catch (SQLTimeoutException ex) { + } catch (SQLTimeoutException | SQLServerException ex) { if (xaLogger.isLoggable(Level.FINER)) xaLogger.finer(toString() + " exception:" + ex); XAException e = new XAException(ex.toString()); e.errorCode = XAException.XAER_RMFAIL; throw e; - } catch (SQLServerException ex) { - if (xaLogger.isLoggable(Level.FINER)) - xaLogger.finer(toString() + " exception:" + ex); - - if (ex.getMessage().equals(SQLServerException.getErrString("R_noServerResponse")) - || TransientError.isTransientError(ex.getSQLServerError()) || isResourceManagerFailure(ex)) { - XAException e = new XAException(ex.toString()); - e.errorCode = XAException.XAER_RMFAIL; - throw e; - } - - XAException e = new XAException(ex.toString()); - e.errorCode = XAException.XAER_RMERR; - throw e; } if (xaLogger.isLoggable(Level.FINER)) @@ -945,68 +929,4 @@ private static int nextResourceID() { return baseResourceID.incrementAndGet(); } - private enum ResourceManagerFailure { - CONN_RESET("Connection reset"), - CONN_RESET_BY_PEER("Connection reset by peer"), - CONN_TIMEOUT("Connection timed out"), - CONN_RESILIENCY_CLIENT_UNRECOVERABLE(SQLServerException.getErrString("R_crClientUnrecoverable")); - - private final String errString; - - ResourceManagerFailure(String errString) { - this.errString = errString; - } - - @Override - public String toString() { - return errString; - } - - static ResourceManagerFailure fromString(String errString) { - for (ResourceManagerFailure resourceManagerFailure : ResourceManagerFailure.values()) { - if (errString.equalsIgnoreCase(resourceManagerFailure.toString())) { - return resourceManagerFailure; - } - } - return null; - } - } - - /** - * Check if the root exception of the throwable should be a XAER_RMFAIL exception - * - * @param throwable - * The exception to check if the root cause should be a XAER_RMFAIL - * - * @return True if XAER_RMFAIL, otherwise false - */ - private boolean isResourceManagerFailure(Throwable throwable) { - Throwable root = Util.getRootCause(throwable); - - if (null == root) { - return false; - } - - if (xaLogger.isLoggable(Level.FINE)) { - xaLogger.fine(toString() + " Resource manager failure root exception: " + root); - } - - ResourceManagerFailure err = ResourceManagerFailure.fromString(root.getMessage()); - - if (null == err) { - return false; - } - - // Add as needed here for future XAER_RMFAIL exceptions - switch (err) { - case CONN_RESET: - case CONN_RESET_BY_PEER: - case CONN_TIMEOUT: - case CONN_RESILIENCY_CLIENT_UNRECOVERABLE: - return true; - default: - return false; - } - } - } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java index 70b9c9aa8..61bda22b7 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java @@ -13,8 +13,6 @@ import java.net.UnknownHostException; import java.text.DecimalFormat; import java.text.MessageFormat; -import java.util.List; -import java.util.ArrayList; import java.util.Date; import java.util.Locale; import java.util.Properties; @@ -1046,23 +1044,6 @@ static char[] bytesToChars(byte[] bytes) { } return chars; } - - /** - * @param throwable - * The exception to find root cause for. - * - * @return The root cause of the exception, otherwise null if null throwable input - */ - static Throwable getRootCause(Throwable throwable) { - final List list = new ArrayList<>(); - - while (throwable != null && !list.contains(throwable)) { - list.add(throwable); - throwable = throwable.getCause(); - } - - return list.isEmpty() ? null : list.get(list.size() - 1); - } } From aa466376a1ee28ed069a8753487ace65b2855f2c Mon Sep 17 00:00:00 2001 From: Terry Chow <32403408+tkyc@users.noreply.github.com> Date: Tue, 19 Mar 2024 15:35:22 -0700 Subject: [PATCH 22/47] Fix calling procedures with output parameters by their four-part syntax (#2349) * Corrected four part syntax regression * JDK 8 correction --- .../jdbc/SQLServerPreparedStatement.java | 20 ++++++- .../CallableStatementTest.java | 60 +++++++++++++++++++ .../sqlserver/testframework/AbstractTest.java | 7 +++ 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index 7789b3726..294a1aa87 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -75,6 +75,8 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements IS private boolean isCallEscapeSyntax; + private boolean isFourPartSyntax; + /** Parameter positions in processed SQL statement text. */ final int[] userSQLParamPositions; @@ -144,6 +146,11 @@ private void setPreparedStatementHandle(int handle) { */ private static final Pattern execEscapePattern = Pattern.compile("^\\s*(?i)(?:exec|execute)\\b"); + /** + * Regex for four part syntax + */ + private static final Pattern fourPartSyntaxPattern = Pattern.compile("(.+)\\.(.+)\\.(.+)\\.(.+)"); + /** Returns the prepared statement SQL */ @Override public String toString() { @@ -271,6 +278,7 @@ private boolean resetPrepStmtHandle(boolean discardCurrentCacheItem) { userSQL = parsedSQL.processedSQL; isExecEscapeSyntax = isExecEscapeSyntax(sql); isCallEscapeSyntax = isCallEscapeSyntax(sql); + isFourPartSyntax = isFourPartSyntax(sql); userSQLParamPositions = parsedSQL.parameterPositions; initParams(userSQLParamPositions.length); useBulkCopyForBatchInsert = conn.getUseBulkCopyForBatchInsert(); @@ -1234,10 +1242,12 @@ boolean callRPCDirectly(Parameter[] params) throws SQLServerException { // 2. There must be parameters // 3. Parameters must not be a TVP type // 4. Compliant CALL escape syntax - // If isExecEscapeSyntax is true, EXEC escape syntax is used then use prior behaviour to - // execute the procedure + // If isExecEscapeSyntax is true, EXEC escape syntax is used then use prior behaviour of + // wrapping call to execute the procedure + // If isFourPartSyntax is true, sproc is being executed against linked server, then + // use prior behaviour of wrapping call to execute procedure return (null != procedureName && paramCount != 0 && !isTVPType(params) && isCallEscapeSyntax - && !isExecEscapeSyntax); + && !isExecEscapeSyntax && !isFourPartSyntax); } /** @@ -1265,6 +1275,10 @@ private boolean isCallEscapeSyntax(String sql) { return callEscapePattern.matcher(sql).find(); } + private boolean isFourPartSyntax(String sql) { + return fourPartSyntaxPattern.matcher(sql).find(); + } + /** * Executes sp_prepare to prepare a parameterized statement and sets the prepared statement handle * diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java index 6570abf72..512039392 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java @@ -1150,6 +1150,66 @@ public void testExecDocumentedSystemStoredProceduresIndexedParameters() throws S } } + @Test + @Tag(Constants.reqExternalSetup) + @Tag(Constants.xAzureSQLDB) + @Tag(Constants.xAzureSQLDW) + @Tag(Constants.xAzureSQLMI) + public void testFourPartSyntaxCallEscapeSyntax() throws SQLException { + String table = "serverList"; + + try (Statement stmt = connection.createStatement()) { + stmt.execute("IF OBJECT_ID(N'" + table + "') IS NOT NULL DROP TABLE " + table); + stmt.execute("CREATE TABLE " + table + " (serverName varchar(100),network varchar(100),serverStatus varchar(4000), id int, collation varchar(100), connectTimeout int, queryTimeout int)"); + stmt.execute("INSERT " + table + " EXEC sp_helpserver"); + + ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM " + table + " WHERE serverName = N'" + linkedServer + "'"); + rs.next(); + + if (rs.getInt(1) == 1) { + stmt.execute("EXEC sp_dropserver @server='" + linkedServer + "';"); + } + + stmt.execute("EXEC sp_addlinkedserver @server='" + linkedServer + "';"); + stmt.execute("EXEC sp_addlinkedsrvlogin @rmtsrvname=N'" + linkedServer + "', @rmtuser=N'" + remoteUser + "', @rmtpassword=N'" + remotePassword + "'"); + stmt.execute("EXEC sp_serveroption '" + linkedServer + "', 'rpc', true;"); + stmt.execute("EXEC sp_serveroption '" + linkedServer + "', 'rpc out', true;"); + } + + SQLServerDataSource ds = new SQLServerDataSource(); + ds.setServerName(linkedServer); + ds.setUser(remoteUser); + ds.setPassword(remotePassword); + ds.setEncrypt(false); + ds.setTrustServerCertificate(true); + + try (Connection linkedServerConnection = ds.getConnection(); Statement stmt = linkedServerConnection.createStatement()) { + stmt.execute("create or alter procedure dbo.TestAdd(@Num1 int, @Num2 int, @Result int output) as begin set @Result = @Num1 + @Num2; end;"); + } + + try (CallableStatement cstmt = connection.prepareCall("{call [" + linkedServer + "].master.dbo.TestAdd(?,?,?)}")) { + int sum = 11; + int param0 = 1; + int param1 = 10; + cstmt.setInt(1, param0); + cstmt.setInt(2, param1); + cstmt.registerOutParameter(3, Types.INTEGER); + cstmt.execute(); + assertEquals(sum, cstmt.getInt(3)); + } + + try (CallableStatement cstmt = connection.prepareCall("exec [" + linkedServer + "].master.dbo.TestAdd ?,?,?")) { + int sum = 11; + int param0 = 1; + int param1 = 10; + cstmt.setInt(1, param0); + cstmt.setInt(2, param1); + cstmt.registerOutParameter(3, Types.INTEGER); + cstmt.execute(); + assertEquals(sum, cstmt.getInt(3)); + } + } + /** * Cleanup after test * diff --git a/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java b/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java index d17fa1469..1ce9cd195 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java @@ -62,6 +62,9 @@ public abstract class AbstractTest { protected static String tenantID; protected static String[] keyIDs = null; + protected static String linkedServer = null; + protected static String remoteUser = null; + protected static String remotePassword = null; protected static String[] enclaveServer = null; protected static String[] enclaveAttestationUrl = null; protected static String[] enclaveAttestationProtocol = null; @@ -197,6 +200,10 @@ public static void setup() throws Exception { clientKeyPassword = getConfiguredProperty("clientKeyPassword", ""); + linkedServer = getConfiguredProperty("linkedServer", null); + remoteUser = getConfiguredProperty("remoteUser", null); + remotePassword = getConfiguredProperty("remotePassword", null); + kerberosServer = getConfiguredProperty("kerberosServer", null); kerberosServerPort = getConfiguredProperty("kerberosServerPort", null); From 1d4b7d659fa694ace8cc07cc954d843dd51380df Mon Sep 17 00:00:00 2001 From: Terry Chow <32403408+tkyc@users.noreply.github.com> Date: Wed, 20 Mar 2024 11:22:06 -0700 Subject: [PATCH 23/47] Added token cache map to fix use of unintended auth token for subsequent connections (#2341) * Added token cache map * Added null check * Removed print debug statements from tests * Corrected return value * Comments * Applied formatting * Increased cache TTL to 24 hrs * Code review comments * Code reivew comments p2 --- .../PersistentTokenCacheAccessAspect.java | 17 +++- .../sqlserver/jdbc/SQLServerMSAL4JUtils.java | 83 +++++++++++++++++-- .../sqlserver/jdbc/TestResource.java | 5 ++ .../sqlserver/jdbc/fedauth/FedauthTest.java | 61 ++++++++++++++ 4 files changed, 155 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/PersistentTokenCacheAccessAspect.java b/src/main/java/com/microsoft/sqlserver/jdbc/PersistentTokenCacheAccessAspect.java index 31b87fb27..e388ac90b 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/PersistentTokenCacheAccessAspect.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/PersistentTokenCacheAccessAspect.java @@ -22,13 +22,16 @@ * @see https://aka.ms/msal4j-token-cache */ public class PersistentTokenCacheAccessAspect implements ITokenCacheAccessAspect { - private static PersistentTokenCacheAccessAspect instance = new PersistentTokenCacheAccessAspect(); - + private static PersistentTokenCacheAccessAspect instance; private final Lock lock = new ReentrantLock(); - private PersistentTokenCacheAccessAspect() {} + static final long TIME_TO_LIVE = 86400000L; // Token cache time to live (24 hrs). + private long expiryTime; static PersistentTokenCacheAccessAspect getInstance() { + if (instance == null) { + instance = new PersistentTokenCacheAccessAspect(); + } return instance; } @@ -62,6 +65,14 @@ public void afterCacheAccess(ITokenCacheAccessContext iTokenCacheAccessContext) } + public long getExpiryTime() { + return this.expiryTime; + } + + public void setExpiryTime(long expiryTime) { + this.expiryTime = expiryTime; + } + /** * Clears User token cache. This will clear all account info so interactive login will be required on the next * request to acquire an access token. diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java index c8f2fb347..1ae69891b 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java @@ -14,6 +14,7 @@ import java.net.URI; import java.net.URISyntaxException; +import java.security.MessageDigest; import java.text.MessageFormat; import java.util.Collections; @@ -21,6 +22,7 @@ import java.util.HashSet; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -61,6 +63,8 @@ class SQLServerMSAL4JUtils { static final String SLASH_DEFAULT = "/.default"; static final String ACCESS_TOKEN_EXPIRE = "access token expires: "; + private static final TokenCacheMap TOKEN_CACHE_MAP = new TokenCacheMap(); + private final static String LOGCONTEXT = "MSAL version " + com.microsoft.aad.msal4j.PublicClientApplication.class.getPackage().getImplementationVersion() + ": "; @@ -84,10 +88,17 @@ static SqlAuthenticationToken getSqlFedAuthToken(SqlFedAuthInfo fedAuthInfo, Str lock.lock(); try { + String hashedSecret = getHashedSecret(new String[] {fedAuthInfo.stsurl, user, password}); + PersistentTokenCacheAccessAspect persistentTokenCacheAccessAspect = TOKEN_CACHE_MAP.getEntry(hashedSecret); + + if (null == persistentTokenCacheAccessAspect) { + persistentTokenCacheAccessAspect = new PersistentTokenCacheAccessAspect(); + TOKEN_CACHE_MAP.addEntry(hashedSecret, persistentTokenCacheAccessAspect); + } + final PublicClientApplication pca = PublicClientApplication .builder(ActiveDirectoryAuthentication.JDBC_FEDAUTH_CLIENT_ID).executorService(executorService) - .setTokenCacheAccessAspect(PersistentTokenCacheAccessAspect.getInstance()) - .authority(fedAuthInfo.stsurl).build(); + .setTokenCacheAccessAspect(persistentTokenCacheAccessAspect).authority(fedAuthInfo.stsurl).build(); final CompletableFuture future = pca.acquireToken(UserNamePasswordParameters .builder(Collections.singleton(fedAuthInfo.spn + SLASH_DEFAULT), user, password.toCharArray()) @@ -132,11 +143,19 @@ static SqlAuthenticationToken getSqlFedAuthTokenPrincipal(SqlFedAuthInfo fedAuth lock.lock(); try { + String hashedSecret = getHashedSecret( + new String[] {fedAuthInfo.stsurl, aadPrincipalID, aadPrincipalSecret}); + PersistentTokenCacheAccessAspect persistentTokenCacheAccessAspect = TOKEN_CACHE_MAP.getEntry(hashedSecret); + + if (null == persistentTokenCacheAccessAspect) { + persistentTokenCacheAccessAspect = new PersistentTokenCacheAccessAspect(); + TOKEN_CACHE_MAP.addEntry(hashedSecret, persistentTokenCacheAccessAspect); + } + IClientCredential credential = ClientCredentialFactory.createFromSecret(aadPrincipalSecret); ConfidentialClientApplication clientApplication = ConfidentialClientApplication .builder(aadPrincipalID, credential).executorService(executorService) - .setTokenCacheAccessAspect(PersistentTokenCacheAccessAspect.getInstance()) - .authority(fedAuthInfo.stsurl).build(); + .setTokenCacheAccessAspect(persistentTokenCacheAccessAspect).authority(fedAuthInfo.stsurl).build(); final CompletableFuture future = clientApplication .acquireToken(ClientCredentialParameters.builder(scopes).build()); @@ -181,6 +200,15 @@ static SqlAuthenticationToken getSqlFedAuthTokenPrincipalCertificate(SqlFedAuthI lock.lock(); try { + String hashedSecret = getHashedSecret(new String[] {fedAuthInfo.stsurl, aadPrincipalID, certFile, + certPassword, certKey, certKeyPassword}); + PersistentTokenCacheAccessAspect persistentTokenCacheAccessAspect = TOKEN_CACHE_MAP.getEntry(hashedSecret); + + if (null == persistentTokenCacheAccessAspect) { + persistentTokenCacheAccessAspect = new PersistentTokenCacheAccessAspect(); + TOKEN_CACHE_MAP.addEntry(hashedSecret, persistentTokenCacheAccessAspect); + } + ConfidentialClientApplication clientApplication = null; // check if cert is PKCS12 first @@ -202,8 +230,7 @@ static SqlAuthenticationToken getSqlFedAuthTokenPrincipalCertificate(SqlFedAuthI IClientCredential credential = ClientCredentialFactory.createFromCertificate(is, certPassword); clientApplication = ConfidentialClientApplication.builder(aadPrincipalID, credential) - .executorService(executorService) - .setTokenCacheAccessAspect(PersistentTokenCacheAccessAspect.getInstance()) + .executorService(executorService).setTokenCacheAccessAspect(persistentTokenCacheAccessAspect) .authority(fedAuthInfo.stsurl).build(); } catch (FileNotFoundException e) { // re-throw if file not there no point to try another format @@ -232,8 +259,7 @@ static SqlAuthenticationToken getSqlFedAuthTokenPrincipalCertificate(SqlFedAuthI IClientCredential credential = ClientCredentialFactory.createFromCertificate(privateKey, cert); clientApplication = ConfidentialClientApplication.builder(aadPrincipalID, credential) - .executorService(executorService) - .setTokenCacheAccessAspect(PersistentTokenCacheAccessAspect.getInstance()) + .executorService(executorService).setTokenCacheAccessAspect(persistentTokenCacheAccessAspect) .authority(fedAuthInfo.stsurl).build(); } @@ -449,4 +475,45 @@ private static SQLServerException getCorrectedException(Exception e, String user return new SQLServerException(form.format(msgArgs), null, 0, correctedExecutionException); } } + + private static String getHashedSecret(String[] secrets) throws SQLServerException { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + for (String secret : secrets) { + if (null != secret) { + md.update(secret.getBytes(java.nio.charset.StandardCharsets.UTF_16LE)); + } + } + return new String(md.digest()); + } catch (NoSuchAlgorithmException e) { + throw new SQLServerException(SQLServerException.getErrString("R_NoSHA256Algorithm"), e); + } + } + + private static class TokenCacheMap { + private ConcurrentHashMap tokenCacheMap = new ConcurrentHashMap<>(); + + PersistentTokenCacheAccessAspect getEntry(String key) { + PersistentTokenCacheAccessAspect persistentTokenCacheAccessAspect = tokenCacheMap.get(key); + + if (null != persistentTokenCacheAccessAspect) { + if (System.currentTimeMillis() > persistentTokenCacheAccessAspect.getExpiryTime()) { + tokenCacheMap.remove(key); + + persistentTokenCacheAccessAspect = new PersistentTokenCacheAccessAspect(); + persistentTokenCacheAccessAspect + .setExpiryTime(System.currentTimeMillis() + PersistentTokenCacheAccessAspect.TIME_TO_LIVE); + + tokenCacheMap.put(key, persistentTokenCacheAccessAspect); + } + } + + return persistentTokenCacheAccessAspect; + } + + void addEntry(String key, PersistentTokenCacheAccessAspect value) { + value.setExpiryTime(System.currentTimeMillis() + PersistentTokenCacheAccessAspect.TIME_TO_LIVE); + tokenCacheMap.put(key, value); + } + } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java index 0e5f26a90..aa4252f95 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java @@ -63,6 +63,11 @@ protected Object[][] getContents() { {"R_ConnectionURLNull", "The connection URL is null."}, {"R_connectionIsNotClosed", "The connection is not closed."}, {"R_invalidExceptionMessage", "Invalid exception message"}, + {"R_invalidClientSecret", "AADSTS7000215: Invalid client secret provided"}, + {"R_invalidCertFields", + "Error reading certificate, please verify the location of the certificate.signed fields invalid"}, + {"R_invalidAADAuth", + "Failed to authenticate the user {0} in Active Directory (Authentication={1})"}, {"R_failedValidate", "failed to validate values in $0} "}, {"R_tableNotDropped", "table not dropped. "}, {"R_connectionReset", "Connection reset"}, {"R_unknownException", "Unknown exception"}, {"R_deadConnection", "Dead connection should be invalid"}, diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthTest.java index 4bae199d2..e68fcacd8 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthTest.java @@ -18,6 +18,7 @@ import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; +import java.text.MessageFormat; import java.util.Collections; import java.util.Properties; import java.util.concurrent.CompletableFuture; @@ -323,6 +324,66 @@ public void testAADServicePrincipalAuth() { } } + @Test + public void testAADServicePrincipalAuthFailureOnSubsequentConnectionsWithInvalidatedTokenCacheWithInvalidSecret() throws Exception { + String url = "jdbc:sqlserver://" + azureServer + ";database=" + azureDatabase + ";authentication=" + + SqlAuthentication.ActiveDirectoryServicePrincipal + ";Username=" + applicationClientID + ";Password=" + + applicationKey; + + String invalidSecretUrl = "jdbc:sqlserver://" + azureServer + ";database=" + azureDatabase + ";authentication=" + + SqlAuthentication.ActiveDirectoryServicePrincipal + ";Username=" + applicationClientID + ";Password=" + + "invalidSecret"; + + // Should succeed on valid secret + try (Connection connection = DriverManager.getConnection(url)) {} + + // Should fail on invalid secret + try (Connection connection = DriverManager.getConnection(invalidSecretUrl)) { + fail(TestResource.getResource("R_expectedFailPassed")); + } catch (Exception e) { + assertTrue(e.getMessage().contains(TestResource.getResource("R_invalidClientSecret")), + "Expected R_invalidClientSecret error."); + } + } + + @Test + public void testActiveDirectoryPasswordFailureOnSubsequentConnectionsWithInvalidatedTokenCacheWithInvalidPassword() throws Exception { + + // Should succeed on valid password + try (Connection conn = DriverManager.getConnection(adPasswordConnectionStr)) {} + + // Should fail on invalid password + try (Connection conn = DriverManager.getConnection(adPasswordConnectionStr + ";password=invalidPassword;")) { + fail(TestResource.getResource("R_expectedFailPassed")); + } catch (Exception e) { + MessageFormat form = new MessageFormat(TestResource.getResource("R_invalidAADAuth")); + Object[] msgArgs = {azureUserName, "ActiveDirectoryPassword"}; + assertTrue(e.getMessage().contains(form.format(msgArgs)), "Expected R_invalidAADAuth error."); + } + } + + @Test + public void testAADServicePrincipalCertAuthFailureOnSubsequentConnectionsWithInvalidatedTokenCacheWithInvalidPassword() throws Exception { + // Should succeed on valid cert field values + String url = "jdbc:sqlserver://" + azureServer + ";database=" + azureDatabase + ";authentication=" + + SqlAuthentication.ActiveDirectoryServicePrincipalCertificate + ";Username=" + applicationClientID + + ";password=" + certificatePassword + ";clientCertificate=" + clientCertificate; + + // Should fail on invalid cert field values + String invalidPasswordUrl = "jdbc:sqlserver://" + azureServer + ";database=" + azureDatabase + + ";authentication=" + SqlAuthentication.ActiveDirectoryServicePrincipalCertificate + ";Username=" + + applicationClientID + ";password=invalidPassword;clientCertificate=" + clientCertificate; + + try (Connection conn = DriverManager.getConnection(url)) {} + + try (Connection conn = DriverManager.getConnection(invalidPasswordUrl)) { + fail(TestResource.getResource("R_expectedFailPassed")); + } catch (Exception e) { + assertTrue(e.getMessage().contains(TestResource.getResource("R_invalidCertFields")), + "Expected R_invalidCertFields error."); + } + } + /** * Test invalid connection property combinations when using AAD Service Principal Authentication. */ From 9a8849b60e74d1d01508fa61f944d9e635cab700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20de=20Launois?= Date: Wed, 20 Mar 2024 23:55:13 +0100 Subject: [PATCH 24/47] Add support for TDSType.GUID (#1582) (#2324) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added support for TDSType.GUID (#1582) * Fix UUID serialisation * Add unit tests * Fix typo, handle PR feedback --------- Co-authored-by: Cédric de Launois Co-authored-by: Cédric de Launois <> --- .../microsoft/sqlserver/jdbc/IOBuffer.java | 27 ++++++ .../microsoft/sqlserver/jdbc/Parameter.java | 5 + .../com/microsoft/sqlserver/jdbc/dtv.java | 21 +++- .../sqlserver/jdbc/datatypes/GuidTest.java | 97 +++++++++++++++++++ 4 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/microsoft/sqlserver/jdbc/datatypes/GuidTest.java diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index 3a5db2bf8..597bc9624 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -57,6 +57,7 @@ import java.util.Set; import java.util.SimpleTimeZone; import java.util.TimeZone; +import java.util.UUID; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; @@ -4784,6 +4785,32 @@ void writeRPCBigDecimal(String sName, BigDecimal bdValue, int nScale, boolean bO writeBytes(val, 0, val.length); } + /** + * Append a UUID in RPC transmission format. + * + * @param sName + * the optional parameter name + * @param uuidValue + * the data value + * @param bOut + * boolean true if the data value is being registered as an output parameter + */ + void writeRPCUUID(String sName, UUID uuidValue, boolean bOut) throws SQLServerException { + writeRPCNameValType(sName, bOut, TDSType.GUID); + + if (uuidValue == null) { + writeByte((byte) 0); + writeByte((byte) 0); + + } else { + writeByte((byte) 0x10); // maximum length = 16 + writeByte((byte) 0x10); // length = 16 + + byte[] val = Util.asGuidByteArray(uuidValue); + writeBytes(val, 0, val.length); + } + } + /** * Appends a standard v*max header for RPC parameter transmission. * diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java b/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java index 4894c45f2..6b61cf53e 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java @@ -22,6 +22,7 @@ import java.time.OffsetTime; import java.util.Calendar; import java.util.Locale; +import java.util.UUID; /** @@ -1125,6 +1126,10 @@ void execute(DTV dtv, Boolean booleanValue) throws SQLServerException { setTypeDefinition(dtv); } + void execute(DTV dtv, UUID uuidValue) throws SQLServerException { + setTypeDefinition(dtv); + } + void execute(DTV dtv, byte[] byteArrayValue) throws SQLServerException { // exclude JDBC typecasting for Geometry/Geography as these datatypes don't have a size limit. if (null != byteArrayValue && byteArrayValue.length > DataTypes.SHORT_VARTYPE_MAX_BYTES diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java index c03d67eb0..32a1c7626 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java @@ -99,6 +99,8 @@ abstract class DTVExecuteOp { abstract void execute(DTV dtv, Boolean booleanValue) throws SQLServerException; + abstract void execute(DTV dtv, UUID uuidValue) throws SQLServerException; + abstract void execute(DTV dtv, byte[] byteArrayValue) throws SQLServerException; abstract void execute(DTV dtv, Blob blobValue) throws SQLServerException; @@ -291,7 +293,11 @@ final class SendByRPCOp extends DTVExecuteOp { } void execute(DTV dtv, String strValue) throws SQLServerException { - tdsWriter.writeRPCStringUnicode(name, strValue, isOutParam, collation, dtv.isNonPLP); + if (dtv.getJdbcType() == JDBCType.GUID) { + tdsWriter.writeRPCUUID(name, UUID.fromString(strValue), isOutParam); + } else { + tdsWriter.writeRPCStringUnicode(name, strValue, isOutParam, collation, dtv.isNonPLP); + } } void execute(DTV dtv, Clob clobValue) throws SQLServerException { @@ -1126,6 +1132,10 @@ void execute(DTV dtv, Boolean booleanValue) throws SQLServerException { tdsWriter.writeRPCBit(name, booleanValue, isOutParam); } + void execute(DTV dtv, UUID uuidValue) throws SQLServerException { + tdsWriter.writeRPCUUID(name, uuidValue, isOutParam); + } + void execute(DTV dtv, byte[] byteArrayValue) throws SQLServerException { if (null != cryptoMeta) { tdsWriter.writeRPCNameValType(name, isOutParam, TDSType.BIGVARBINARY); @@ -1537,10 +1547,13 @@ final void executeOp(DTVExecuteOp op) throws SQLServerException { case VARCHAR: case LONGVARCHAR: case CLOB: - case GUID: op.execute(this, (byte[]) null); break; + case GUID: + op.execute(this, (UUID) null); + break; + case TINYINT: if (null != cryptoMeta) op.execute(this, (byte[]) null); @@ -1619,7 +1632,7 @@ final void executeOp(DTVExecuteOp op) throws SQLServerException { byte[] bArray = Util.asGuidByteArray((UUID) value); op.execute(this, bArray); } else { - op.execute(this, String.valueOf(value)); + op.execute(this, UUID.fromString(String.valueOf(value))); } } else if (JDBCType.SQL_VARIANT == jdbcType) { op.execute(this, String.valueOf(value)); @@ -2194,6 +2207,8 @@ void execute(DTV dtv, Short shortValue) throws SQLServerException {} void execute(DTV dtv, Boolean booleanValue) throws SQLServerException {} + void execute(DTV dtv, UUID uuidValue) throws SQLServerException {} + void execute(DTV dtv, byte[] byteArrayValue) throws SQLServerException {} void execute(DTV dtv, Blob blobValue) throws SQLServerException { diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/GuidTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/GuidTest.java new file mode 100644 index 000000000..e8b5c157b --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/GuidTest.java @@ -0,0 +1,97 @@ +package com.microsoft.sqlserver.jdbc.datatypes; + +import com.microsoft.sqlserver.jdbc.RandomUtil; +import com.microsoft.sqlserver.jdbc.SQLServerResultSet; +import com.microsoft.sqlserver.jdbc.TestResource; +import com.microsoft.sqlserver.jdbc.TestUtils; +import com.microsoft.sqlserver.testframework.AbstractSQLGenerator; +import com.microsoft.sqlserver.testframework.AbstractTest; +import microsoft.sql.Types; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.Statement; +import java.util.UUID; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/* + * This test is for testing the serialisation of String as microsoft.sql.Types.GUID + */ +@RunWith(JUnitPlatform.class) +public class GuidTest extends AbstractTest { + + final static String tableName = RandomUtil.getIdentifier("GuidTestTable"); + final static String escapedTableName = AbstractSQLGenerator.escapeIdentifier(tableName); + + @BeforeAll + public static void setupTests() throws Exception { + setConnection(); + } + + /* + * Test UUID conversions + */ + @Test + public void testGuid() throws Exception { + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { + + // Create the test table + TestUtils.dropTableIfExists(escapedTableName, stmt); + + String query = "create table " + escapedTableName + + " (uuid uniqueidentifier, id int IDENTITY primary key)"; + stmt.executeUpdate(query); + + UUID uuid = UUID.randomUUID(); + String uuidString = uuid.toString(); + int id = 1; + + try (PreparedStatement pstmt = conn.prepareStatement("INSERT INTO " + escapedTableName + + " VALUES(?) SELECT * FROM " + escapedTableName + " where id = ?")) { + + pstmt.setObject(1, uuidString, Types.GUID); + pstmt.setObject(2, id++); + pstmt.execute(); + pstmt.getMoreResults(); + try (SQLServerResultSet rs = (SQLServerResultSet) pstmt.getResultSet()) { + rs.next(); + assertEquals(uuid, UUID.fromString(rs.getUniqueIdentifier(1))); + } + + // Test NULL GUID + pstmt.setObject(1, null, Types.GUID); + pstmt.setObject(2, id++); + pstmt.execute(); + pstmt.getMoreResults(); + try (SQLServerResultSet rs = (SQLServerResultSet) pstmt.getResultSet()) { + rs.next(); + String s = rs.getUniqueIdentifier(1); + assertNull(s); + assertTrue(rs.wasNull()); + } + + // Test Illegal GUID + try { + pstmt.setObject(1, "garbage", Types.GUID); + fail(TestResource.getResource("R_expectedFailPassed")); + } catch (IllegalArgumentException e) { + assertEquals("Invalid UUID string: garbage", e.getMessage()); + } + } + } finally { + try (Statement stmt = connection.createStatement()) { + TestUtils.dropTableIfExists(escapedTableName, stmt); + } + } + } + +} From c74a51e95c5542a285c40cac1706b01850320da0 Mon Sep 17 00:00:00 2001 From: Goran Schwarz Date: Fri, 22 Mar 2024 23:51:51 +0100 Subject: [PATCH 25/47] Server Message Handler and SQLException Chaining (#2251) * first commit on: ServerMessageHandler - to intercept/ignore/up-grade/down-grade serv messages, and Chained Exceptions so we can see many srv messages in a SQLException * Added 3 classes * And the changes made to existing files * Removed get/set ServerMessageHandler from ISQLServerStatement and SQLServerStatement Simplified some code and formatted it to meet requirements Added test cases * smaller simplifications * fixed Copyright in one file! Also fixed smaller code format mishaps * Fixed code formatting and removed some stuff from the test cases * removed public for my get/setMessageHandler from interface ISQLServerConnection * Code formatting * Changed a cast in 'SQLServerStatement' from 'SQLServerConnection' to 'ISQLServerConnection' this might solve my strange problem! Also added get/setServerMessageHandler to getVerifiedMethodNames() in RequestBoundaryMethodsTest.java * Update SQLServerConnectionPoolProxy.java * Made some changes requested by reviewer: lilgreenbird --------- Co-authored-by: Jeffery Wasty --- .../sqlserver/jdbc/ISQLServerConnection.java | 13 + .../sqlserver/jdbc/ISQLServerMessage.java | 93 ++++ .../jdbc/ISQLServerMessageHandler.java | 103 ++++ .../sqlserver/jdbc/SQLServerConnection.java | 49 ++ .../jdbc/SQLServerConnectionPoolProxy.java | 10 + .../sqlserver/jdbc/SQLServerError.java | 117 ++++- .../sqlserver/jdbc/SQLServerException.java | 25 + .../sqlserver/jdbc/SQLServerInfoMessage.java | 136 +++++ .../sqlserver/jdbc/SQLServerStatement.java | 42 +- .../sqlserver/jdbc/SQLServerWarning.java | 47 ++ .../microsoft/sqlserver/jdbc/StreamInfo.java | 20 - .../microsoft/sqlserver/jdbc/tdsparser.java | 34 +- .../RequestBoundaryMethodsTest.java | 4 + .../jdbc/exception/ChainedExceptionTest.java | 71 +++ .../messageHandler/MessageHandlerTest.java | 497 ++++++++++++++++++ 15 files changed, 1227 insertions(+), 34 deletions(-) create mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessage.java create mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessageHandler.java create mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/SQLServerInfoMessage.java create mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/SQLServerWarning.java delete mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/StreamInfo.java create mode 100644 src/test/java/com/microsoft/sqlserver/jdbc/exception/ChainedExceptionTest.java create mode 100644 src/test/java/com/microsoft/sqlserver/jdbc/messageHandler/MessageHandlerTest.java diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java index 2f54a98ab..a6054b1fc 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java @@ -483,6 +483,19 @@ CallableStatement prepareCall(String sql, int nType, int nConcur, int nHold, */ void setAccessTokenCallbackClass(String accessTokenCallbackClass); + /** + * Get Currently installed message handler on the connection + * @see {@link ISQLServerMessageHandler#messageHandler(ISQLServerMessage)} + * @return + */ + ISQLServerMessageHandler getServerMessageHandler(); + + /** + * Set message handler on the connection + * @see {@link ISQLServerMessageHandler#messageHandler(ISQLServerMessage)} + */ + 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 000000000..055e812b2 --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessage.java @@ -0,0 +1,93 @@ +/* + * 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; + +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 getProcedureName() as + * received from SQL Server + * + * @return Line Number + */ + public long getLineNumber(); + + /** + * Creates a SQLServerException or SQLServerWarning from this SQLServerMessage
+ * @return + *
    + *
  • SQLServerException if it's a SQLServerError object
  • + *
  • SQLServerWarning if it's a SQLServerInfoMessage object
  • + *
+ */ + 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 000000000..74d7d7518 --- /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: + *
    + *
  • "message feedback"
    + * Display Server messages from a long running SQL Statement
    + * Like RAISERROR ('Progress message...', 0, 1) WITH NOWAIT
    + * Or Status messages from a running backup...
    + *
  • + *
  • "Universal" error logging
    + * Your error-message handler can contain the logic for handling all error logging. + *
  • + *
  • "Universal" error handling
    + * Error-handling logic can be placed in your error-message handler, instead of being repeated throughout your application. + *
  • + *
  • Remapping of error-message severity, based on application requirements
    + * 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. + *
  • + *
+ *

+ * 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: + *

    + *
  • "message feedback"
    + * Display Server messages from a long running SQL Statement
    + * Like RAISERROR ('Progress message...', 0, 1) WITH NOWAIT
    + * Or Status messages from a running backup...
    + *
  • + *
  • "Universal" error logging
    + * Your error-message handler can contain the logic for handling all error logging. + *
  • + *
  • "Universal" error handling
    + * Error-handling logic can be placed in your error-message handler, instead of being repeated throughout your application. + *
  • + *
  • Remapping of error-message severity, based on application requirements
    + * 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. + *
  • + *
+ * + * Example code: + *
+     *  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;
+     *  }
+    
+     * 
+ * + * @param serverErrorOrWarning + * @return + *
    + *
  • unchanged same object as passed in.
    + * The JDBC driver will works as if no message hander was installed
    + * Possibly used for logging functionality
    + *
  • + *
  • null
    + * The JDBC driver will discard this message. No SQLException will be thrown + *
  • + *
  • SQLServerInfoMessage object
    + * Create a "SQL warning" from a input database error, and return it. + * This results in the warning being added to the warning-message chain. + *
  • + *
  • SQLServerError object
    + * 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. + *
  • + *
+ */ + ISQLServerMessage messageHandler(ISQLServerMessage serverErrorOrWarning); +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index b20b6e7f8..19f72cbc1 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -4927,6 +4927,27 @@ void addWarning(String warningString) { } } + // 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 +8520,34 @@ public void setIPAddressPreference(String iPAddressPreference) { public String getIPAddressPreference() { return activeConnectionProperties.getProperty(SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString()); } + + + + /** Message handler */ + private transient ISQLServerMessageHandler serverMessageHandler; + + /** + * Set current message handler + * + * @param messageHandler + * @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 ffda2dd6b..5ed4b7b06 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 @@ public void setAccessTokenCallbackClass(String accessTokenCallbackClass) { 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/SQLServerError.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerError.java index 3454e5aef..1b4c1e836 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 long getLineNumber() { 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,104 @@ void setContentsFromTDS(TDSReader tdsReader) throws SQLServerException { procName = tdsReader.readUnicodeString(tdsReader.readUnsignedByte()); lineNumber = tdsReader.readUnsignedInt(); } + + + + /** + * Holds any "overflow messages", or messages that has been added after the first message. + *

+ * This is later on used when creating a SQLServerException.
+ * Where all entries in the errorChain will be added {@link java.sql.SQLException#setNextException(SQLException)} + */ + private List errorChain; + + void addError(SQLServerError sqlServerError) { + if (errorChain == null) { + errorChain = new ArrayList<>(); + } + errorChain.add(sqlServerError); + } + + List getErrorChain() { + return errorChain; + } + + @Override + public SQLServerError getSQLServerMessage() + { + return this; + } + + /** + * Downgrade a Error message into a Info message + *

+ * This simply create a SQLServerInfoMessage from this SQLServerError, + * without changing the message content. + * @return + */ + public ISQLServerMessage toSQLServerInfoMessage() + { + return toSQLServerInfoMessage(-1, -1); + } + + /** + * Downgrade a Error message into a Info message + *

+ * This simply create a SQLServerInfoMessage from this SQLServerError, + * + * @param newErrorSeverity - The new ErrorSeverity + * + * @return + */ + public ISQLServerMessage toSQLServerInfoMessage(int newErrorSeverity) + { + return toSQLServerInfoMessage(newErrorSeverity, -1); + } + + /** + * Downgrade a Error message into a Info message + *

+ * 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 + */ + 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 + */ + public void setErrorSeverity(int newSeverity) + { + this.errorSeverity = newSeverity; + } + + /** + * Set a new ErrorNumber for this Message + * @param newSeverity + */ + 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 943804dcf..77367708a 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 @@ 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 @@ static String getErrString(String errCode) { 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 @@ static void makeFromDatabaseError(SQLServerConnection con, Object obj, String er SQLServerException.checkAndAppendClientConnId(errText, con), state, sqlServerError, bStack); theException.setDriverErrorCode(DRIVER_ERROR_FROM_DATABASE); + // Add any extra messages to the SQLException error chain + List 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 000000000..8b6579491 --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerInfoMessage.java @@ -0,0 +1,136 @@ +/* + * 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) + *

+ * 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 + *

+ * This enables client to print out extra information about the message.
+ * Like: In what procedure was the message produced. + *

+ * 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 + *

+ * This simply create a SQLServerError from this SQLServerInfoMessage, + * without changing the message content. + * @return + */ + public ISQLServerMessage toSQLServerError() + { + return toSQLServerError(-1, -1); + } + + /** + * Upgrade a Info message into a Error message + *

+ * This simply create a SQLServerError from this SQLServerInfoMessage. + * + * @param newErrorSeverity - The new ErrorSeverity + * + * @return + */ + public ISQLServerMessage toSQLServerError(int newErrorSeverity) + { + return toSQLServerError(newErrorSeverity, -1); + } + + /** + * Upgrade a Info message into a Error message + *

+ * 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 + */ + 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/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java index 866b9ea13..cbfe102ca 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 @@ boolean onDone(TDSReader tdsReader) throws SQLServerException { } } } - // 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 @@ boolean onRetValue(TDSReader tdsReader) throws SQLServerException { @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 @@ boolean onInfo(TDSReader tdsReader) throws SQLServerException { // 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 000000000..81d588bd9 --- /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) + *

+ * 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 + *

+ * This enables client to print out extra information about the message.
+ * 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 5b4cb571f..000000000 --- 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/tdsparser.java b/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java index c18d61a0c..90eebb8df 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 @@ final SQLServerError getDatabaseError() { 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 @@ boolean onDone(TDSReader tdsReader) throws SQLServerException { } 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; } 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 d758950f8..926b6a766 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 @@ private List getVerifiedMethodNames() { 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/exception/ChainedExceptionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/exception/ChainedExceptionTest.java new file mode 100644 index 000000000..9e6b1d4b7 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/exception/ChainedExceptionTest.java @@ -0,0 +1,71 @@ +/* + * 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.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.TestResource; +import com.microsoft.sqlserver.testframework.AbstractTest; + + +@RunWith(JUnitPlatform.class) +public class ChainedExceptionTest extends AbstractTest { + + @BeforeAll + public static void setupTests() throws Exception { + setConnection(); + } + + @Test + 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 000000000..94042a7d8 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/messageHandler/MessageHandlerTest.java @@ -0,0 +1,497 @@ +/* + * 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 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; + + +@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 + *

    + *
  • Insert duplicate row -- Mapped to Info Message
  • + *
  • Drop table that do not exist -- Mapped to ignore
  • + *
+ */ + @Test + 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")); + } + } + + + + /** + * Test message handler with PreparedStatement + *
    + *
  • Insert duplicate row -- Mapped to Info Message
  • + *
  • Drop table that do not exist -- Mapped to ignore
  • + *
+ */ + @Test + 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")); + } + } + + + + /** + * Test message handler with CallableStatement + *
    + *
  • Insert duplicate row -- Mapped to Info Message
  • + *
  • Drop table that do not exist -- Mapped to ignore
  • + *
+ */ + @Test + 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")); + } + } + + + + + /** + * Test message handler with CallableStatement -- and "feedback" messages + *

+ * Do a "long running procedure" and check that the message handler receives the "feedback" messages + */ + @Test + public void testMsgHandlerWithProcedureFeedback() throws Exception { + try (SQLServerConnection conn = getConnection()) { + + class TestMsgHandler implements ISQLServerMessageHandler + { + int numOfCalls = 0; + Map 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 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")); + } + } + +} From 9a13e5d915fe48dbc89259517bab806b7a5b4203 Mon Sep 17 00:00:00 2001 From: lilgreenbird Date: Mon, 25 Mar 2024 11:24:10 -0700 Subject: [PATCH 26/47] javadoc fixes (#2363) --- .../sqlserver/jdbc/ISQLServerConnection.java | 13 +- .../sqlserver/jdbc/ISQLServerMessage.java | 18 ++- .../jdbc/ISQLServerMessageHandler.java | 146 +++++++++--------- .../PersistentTokenCacheAccessAspect.java | 11 ++ ...ColumnEncryptionAzureKeyVaultProvider.java | 16 ++ ...rColumnEncryptionJavaKeyStoreProvider.java | 16 ++ .../sqlserver/jdbc/SQLServerConnection.java | 20 +-- .../jdbc/SQLServerDatabaseMetaData.java | 3 + .../sqlserver/jdbc/SQLServerError.java | 75 +++++---- .../sqlserver/jdbc/SQLServerInfoMessage.java | 57 ++++--- .../jdbc/SQLServerPreparedStatement.java | 3 + 11 files changed, 218 insertions(+), 160 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java index a6054b1fc..e0e4eebbf 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java @@ -485,17 +485,22 @@ CallableStatement prepareCall(String sql, int nType, int nConcur, int nHold, /** * Get Currently installed message handler on the connection - * @see {@link ISQLServerMessageHandler#messageHandler(ISQLServerMessage)} - * @return + * + * @see ISQLServerMessageHandler#messageHandler(ISQLServerMessage) + * @return ISQLServerMessageHandler */ ISQLServerMessageHandler getServerMessageHandler(); /** * Set message handler on the connection - * @see {@link ISQLServerMessageHandler#messageHandler(ISQLServerMessage)} + * + * @param messageHandler + * message handler + * + * @see ISQLServerMessageHandler#messageHandler(ISQLServerMessage) */ 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 index 055e812b2..622b865c3 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessage.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessage.java @@ -6,8 +6,11 @@ import java.sql.SQLException; -public interface ISQLServerMessage -{ + +/** + * Provides an interface SQLServerMessage + */ +public interface ISQLServerMessage { /** * Returns SQLServerError containing detailed info about SQL Server Message as received from SQL Server. * @@ -67,16 +70,18 @@ public interface ISQLServerMessage /** * Creates a SQLServerException or SQLServerWarning from this SQLServerMessage
+ * * @return - *

    - *
  • SQLServerException if it's a SQLServerError object
  • - *
  • SQLServerWarning if it's a SQLServerInfoMessage object
  • - *
+ *
    + *
  • SQLServerException if it's a SQLServerError object
  • + *
  • SQLServerWarning if it's a SQLServerInfoMessage object
  • + *
*/ public SQLException toSqlExceptionOrSqlWarning(); /** * Check if this is a isErrorMessage + * * @return true if it's an instance of SQLServerError */ public default boolean isErrorMessage() { @@ -85,6 +90,7 @@ public default boolean isErrorMessage() { /** * Check if this is a SQLServerInfoMessage + * * @return true if it's an instance of SQLServerInfoMessage */ public default boolean isInfoMessage() { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessageHandler.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessageHandler.java index 74d7d7518..2f0b71de7 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessageHandler.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessageHandler.java @@ -5,99 +5,99 @@ package com.microsoft.sqlserver.jdbc; /** - * You can use the ISQLServerMessageHandler interface to customize the way JDBC handles error messages generated by the SQL Server. + * 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: *
    - *
  • "message feedback"
    - * Display Server messages from a long running SQL Statement
    - * Like RAISERROR ('Progress message...', 0, 1) WITH NOWAIT
    - * Or Status messages from a running backup...
    - *
  • - *
  • "Universal" error logging
    - * Your error-message handler can contain the logic for handling all error logging. - *
  • - *
  • "Universal" error handling
    - * Error-handling logic can be placed in your error-message handler, instead of being repeated throughout your application. - *
  • - *
  • Remapping of error-message severity, based on application requirements
    - * 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. - *
  • + *
  • "message feedback"
    + * Display Server messages from a long running SQL Statement
    + * Like RAISERROR ('Progress message...', 0, 1) WITH NOWAIT
    + * Or Status messages from a running backup...
    + *
  • + *
  • "Universal" error logging
    + * Your error-message handler can contain the logic for handling all error logging. + *
  • + *
  • "Universal" error handling
    + * Error-handling logic can be placed in your error-message handler, instead of being repeated throughout your application. + *
  • + *
  • Remapping of error-message severity, based on application requirements
    + * 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. + *
  • *
*

* For example code, see {@link #messageHandler(ISQLServerMessage)} */ -public interface ISQLServerMessageHandler -{ +public interface ISQLServerMessageHandler { /** - * You can use the ISQLServerMessageHandler interface to customize the way JDBC handles error messages generated by the SQL Server. + * 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: *

    - *
  • "message feedback"
    - * Display Server messages from a long running SQL Statement
    - * Like RAISERROR ('Progress message...', 0, 1) WITH NOWAIT
    - * Or Status messages from a running backup...
    - *
  • - *
  • "Universal" error logging
    - * Your error-message handler can contain the logic for handling all error logging. - *
  • - *
  • "Universal" error handling
    - * Error-handling logic can be placed in your error-message handler, instead of being repeated throughout your application. - *
  • - *
  • Remapping of error-message severity, based on application requirements
    - * 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. - *
  • + *
  • "message feedback"
    + * Display Server messages from a long running SQL Statement
    + * Like RAISERROR ('Progress message...', 0, 1) WITH NOWAIT
    + * Or Status messages from a running backup...
    + *
  • + *
  • "Universal" error logging
    + * Your error-message handler can contain the logic for handling all error logging. + *
  • + *
  • "Universal" error handling
    + * Error-handling logic can be placed in your error-message handler, instead of being repeated throughout your application. + *
  • + *
  • Remapping of error-message severity, based on application requirements
    + * 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. + *
  • *
* * Example code: + * *
-     *  public ISQLServerMessage messageHandler(ISQLServerMessage serverErrorOrWarning)
-     *  {
-     *      ISQLServerMessage retObj = serverErrorOrWarning;
+     * public ISQLServerMessage messageHandler(ISQLServerMessage serverErrorOrWarning) {
+     *     ISQLServerMessage retObj = serverErrorOrWarning;
      *
-     *      if (serverErrorOrWarning.isErrorMessage()) {
+     *     if (serverErrorOrWarning.isErrorMessage()) {
      *
-     *          // Downgrade: 2601 -- Cannot insert duplicate key row...
-     *          if (2601 == serverErrorOrWarning.getErrorNumber()) {
-     *              retObj = serverErrorOrWarning.getSQLServerMessage().toSQLServerInfoMessage();
-     *          }
+     *         // 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;
-     *          }
-     *      }
+     *         // Discard: 3701 -- Cannot drop the table ...
+     *         if (3701 == serverErrorOrWarning.getErrorNumber()) {
+     *             retObj = null;
+     *         }
+     *     }
      *
-     *      return retObj;
-     *  }
-    
+     *     return retObj;
+     * }
+     * 
      * 
* * @param serverErrorOrWarning - * @return - *
    - *
  • unchanged same object as passed in.
    - * The JDBC driver will works as if no message hander was installed
    - * Possibly used for logging functionality
    - *
  • - *
  • null
    - * The JDBC driver will discard this message. No SQLException will be thrown - *
  • - *
  • SQLServerInfoMessage object
    - * Create a "SQL warning" from a input database error, and return it. - * This results in the warning being added to the warning-message chain. - *
  • - *
  • SQLServerError object
    - * 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. - *
  • - *
+ * server error or warning + * @return + *
    + *
  • unchanged same object as passed in.
    + * The JDBC driver will work as if no message hander was installed
    + * Possibly used for logging functionality
    + *
  • + *
  • null
    + * The JDBC driver will discard this message. No SQLException will be thrown + *
  • + *
  • SQLServerInfoMessage object
    + * Create a "SQL warning" from a input database error, and return it. + * This results in the warning being added to the warning-message chain. + *
  • + *
  • SQLServerError object
    + * 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. + *
  • + *
*/ 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 e388ac90b..24458a7f3 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 void afterCacheAccess(ITokenCacheAccessContext iTokenCacheAccessContext) } + /** + * 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/SQLServerColumnEncryptionAzureKeyVaultProvider.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java index 89a433f08..42281688a 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 boolean verifyColumnMasterKeyMetadata(String masterKeyPath, boolean allow 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 50359f04a..d2e2f28c4 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 boolean verifyColumnMasterKeyMetadata(String masterKeyPath, boolean allow 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 19f72cbc1..8012bea5c 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 void setIgnoreOffsetOnDateTimeOffsetConversion(boolean ignoreOffsetOnDate 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(); @@ -8521,31 +8524,28 @@ 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; + 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() - { + public ISQLServerMessageHandler getServerMessageHandler() { return this.serverMessageHandler; } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java index 2dfb40d89..5c23a4777 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 @@ private void checkClosed() throws SQLServerException { // Use LinkedHashMap to force retrieve elements in order they were inserted /** getColumns columns */ private LinkedHashMap getColumnsDWColumns = null; + + /** getTypes columns */ private LinkedHashMap getTypesDWColumns = null; + /** getImportedKeys columns */ private volatile LinkedHashMap 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 1b4c1e836..b3e96904f 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerError.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerError.java @@ -226,15 +226,15 @@ public long getLineNumber() { SQLServerError(SQLServerError errorMsg) { super(TDS.TDS_ERR); - this.errorNumber = errorMsg.errorNumber; - this.errorState = errorMsg.errorState; + 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; + 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()) @@ -253,16 +253,14 @@ void setContentsFromTDS(TDSReader tdsReader) throws SQLServerException { lineNumber = tdsReader.readUnsignedInt(); } - - - /** + /** * Holds any "overflow messages", or messages that has been added after the first message. *

* This is later on used when creating a SQLServerException.
* Where all entries in the errorChain will be added {@link java.sql.SQLException#setNextException(SQLException)} */ private List errorChain; - + void addError(SQLServerError sqlServerError) { if (errorChain == null) { errorChain = new ArrayList<>(); @@ -273,51 +271,51 @@ void addError(SQLServerError sqlServerError) { List getErrorChain() { return errorChain; } - + @Override - public SQLServerError getSQLServerMessage() - { + public SQLServerError getSQLServerMessage() { return this; } /** * Downgrade a Error message into a Info message *

- * This simply create a SQLServerInfoMessage from this SQLServerError, - * without changing the message content. - * @return + * This simply create a SQLServerInfoMessage from this SQLServerError, + * without changing the message content. + * + * @return ISQLServerMessage */ - public ISQLServerMessage toSQLServerInfoMessage() - { + public ISQLServerMessage toSQLServerInfoMessage() { return toSQLServerInfoMessage(-1, -1); } /** * Downgrade a Error message into a Info message *

- * This simply create a SQLServerInfoMessage from this SQLServerError, + * This simply create a SQLServerInfoMessage from this SQLServerError, * - * @param newErrorSeverity - The new ErrorSeverity + * @param newErrorSeverity + * - The new ErrorSeverity * - * @return + * @return ISQLServerMessage */ - public ISQLServerMessage toSQLServerInfoMessage(int newErrorSeverity) - { + public ISQLServerMessage toSQLServerInfoMessage(int newErrorSeverity) { return toSQLServerInfoMessage(newErrorSeverity, -1); } /** * Downgrade a Error message into a Info message *

- * This simply create a SQLServerInfoMessage from this SQLServerError, + * 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) + * @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 + * @return ISQLServerMessage */ - public ISQLServerMessage toSQLServerInfoMessage(int newErrorSeverity, int newErrorNumber) - { + public ISQLServerMessage toSQLServerInfoMessage(int newErrorSeverity, int newErrorNumber) { if (newErrorSeverity != -1) { this.setErrorSeverity(newErrorSeverity); } @@ -331,25 +329,26 @@ public ISQLServerMessage toSQLServerInfoMessage(int newErrorSeverity, int newErr /** * Set a new ErrorSeverity for this Message + * * @param newSeverity + * new severity */ - public void setErrorSeverity(int newSeverity) - { + public void setErrorSeverity(int newSeverity) { this.errorSeverity = newSeverity; } /** * Set a new ErrorNumber for this Message - * @param newSeverity + * + * @param newErrorNumber + * new error number */ - public void setErrorNumber(int newErrorNumber) - { + public void setErrorNumber(int newErrorNumber) { this.errorNumber = newErrorNumber; } - + @Override - public SQLException toSqlExceptionOrSqlWarning() - { + public SQLException toSqlExceptionOrSqlWarning() { return new SQLServerException(this); } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerInfoMessage.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerInfoMessage.java index 8b6579491..0b3a6075a 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerInfoMessage.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerInfoMessage.java @@ -7,10 +7,11 @@ import java.sql.SQLException; + /** * Holds information about SQL Server messages that is considered as Informational Messages (normally if SQL Server Severity is at 10) *

- * Instead of just holding the SQL Server message (like a normal SQLWarning, it also holds all the + * 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 *

* This enables client to print out extra information about the message.
@@ -46,77 +47,76 @@ public SQLServerError getSQLServerMessage() { 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() - { + public String getProcedureName() { return msg.getProcedureName(); } - + @Override - public long getLineNumber() - { + public long getLineNumber() { return msg.getLineNumber(); } /** * Upgrade a Info message into a Error message *

- * This simply create a SQLServerError from this SQLServerInfoMessage, - * without changing the message content. - * @return + * This simply create a SQLServerError from this SQLServerInfoMessage, + * without changing the message content. + * + * @return ISQLServerMessage */ - public ISQLServerMessage toSQLServerError() - { + public ISQLServerMessage toSQLServerError() { return toSQLServerError(-1, -1); } /** * Upgrade a Info message into a Error message *

- * This simply create a SQLServerError from this SQLServerInfoMessage. + * This simply create a SQLServerError from this SQLServerInfoMessage. * - * @param newErrorSeverity - The new ErrorSeverity + * @param newErrorSeverity + * - The new ErrorSeverity * - * @return + * @return ISQLServerMessage */ - public ISQLServerMessage toSQLServerError(int newErrorSeverity) - { + public ISQLServerMessage toSQLServerError(int newErrorSeverity) { return toSQLServerError(newErrorSeverity, -1); } /** * Upgrade a Info message into a Error message *

- * This simply create a SQLServerError from this SQLServerInfoMessage. + * 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) + * @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 + * @return ISQLServerMessage */ - public ISQLServerMessage toSQLServerError(int newErrorSeverity, int newErrorNumber) - { + public ISQLServerMessage toSQLServerError(int newErrorSeverity, int newErrorNumber) { if (newErrorSeverity != -1) { this.msg.setErrorSeverity(newErrorSeverity); } @@ -129,8 +129,7 @@ public ISQLServerMessage toSQLServerError(int newErrorSeverity, int newErrorNumb } @Override - public SQLException toSqlExceptionOrSqlWarning() - { + 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 294a1aa87..4274e6913 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. */ From 8e92248afa6dce48d79ae87bd5809ae02a429b84 Mon Sep 17 00:00:00 2001 From: Terry Chow <32403408+tkyc@users.noreply.github.com> Date: Mon, 25 Mar 2024 12:15:21 -0700 Subject: [PATCH 27/47] Revert "Add support for TDSType.GUID (#1582) (#2324)" (#2365) This reverts commit 9a8849b60e74d1d01508fa61f944d9e635cab700. --- .../microsoft/sqlserver/jdbc/IOBuffer.java | 27 ------ .../microsoft/sqlserver/jdbc/Parameter.java | 5 - .../com/microsoft/sqlserver/jdbc/dtv.java | 21 +--- .../sqlserver/jdbc/datatypes/GuidTest.java | 97 ------------------- 4 files changed, 3 insertions(+), 147 deletions(-) delete mode 100644 src/test/java/com/microsoft/sqlserver/jdbc/datatypes/GuidTest.java diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index 597bc9624..3a5db2bf8 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -57,7 +57,6 @@ import java.util.Set; import java.util.SimpleTimeZone; import java.util.TimeZone; -import java.util.UUID; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; @@ -4785,32 +4784,6 @@ void writeRPCBigDecimal(String sName, BigDecimal bdValue, int nScale, boolean bO writeBytes(val, 0, val.length); } - /** - * Append a UUID in RPC transmission format. - * - * @param sName - * the optional parameter name - * @param uuidValue - * the data value - * @param bOut - * boolean true if the data value is being registered as an output parameter - */ - void writeRPCUUID(String sName, UUID uuidValue, boolean bOut) throws SQLServerException { - writeRPCNameValType(sName, bOut, TDSType.GUID); - - if (uuidValue == null) { - writeByte((byte) 0); - writeByte((byte) 0); - - } else { - writeByte((byte) 0x10); // maximum length = 16 - writeByte((byte) 0x10); // length = 16 - - byte[] val = Util.asGuidByteArray(uuidValue); - writeBytes(val, 0, val.length); - } - } - /** * Appends a standard v*max header for RPC parameter transmission. * diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java b/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java index 6b61cf53e..4894c45f2 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java @@ -22,7 +22,6 @@ import java.time.OffsetTime; import java.util.Calendar; import java.util.Locale; -import java.util.UUID; /** @@ -1126,10 +1125,6 @@ void execute(DTV dtv, Boolean booleanValue) throws SQLServerException { setTypeDefinition(dtv); } - void execute(DTV dtv, UUID uuidValue) throws SQLServerException { - setTypeDefinition(dtv); - } - void execute(DTV dtv, byte[] byteArrayValue) throws SQLServerException { // exclude JDBC typecasting for Geometry/Geography as these datatypes don't have a size limit. if (null != byteArrayValue && byteArrayValue.length > DataTypes.SHORT_VARTYPE_MAX_BYTES diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java index 32a1c7626..c03d67eb0 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java @@ -99,8 +99,6 @@ abstract class DTVExecuteOp { abstract void execute(DTV dtv, Boolean booleanValue) throws SQLServerException; - abstract void execute(DTV dtv, UUID uuidValue) throws SQLServerException; - abstract void execute(DTV dtv, byte[] byteArrayValue) throws SQLServerException; abstract void execute(DTV dtv, Blob blobValue) throws SQLServerException; @@ -293,11 +291,7 @@ final class SendByRPCOp extends DTVExecuteOp { } void execute(DTV dtv, String strValue) throws SQLServerException { - if (dtv.getJdbcType() == JDBCType.GUID) { - tdsWriter.writeRPCUUID(name, UUID.fromString(strValue), isOutParam); - } else { - tdsWriter.writeRPCStringUnicode(name, strValue, isOutParam, collation, dtv.isNonPLP); - } + tdsWriter.writeRPCStringUnicode(name, strValue, isOutParam, collation, dtv.isNonPLP); } void execute(DTV dtv, Clob clobValue) throws SQLServerException { @@ -1132,10 +1126,6 @@ void execute(DTV dtv, Boolean booleanValue) throws SQLServerException { tdsWriter.writeRPCBit(name, booleanValue, isOutParam); } - void execute(DTV dtv, UUID uuidValue) throws SQLServerException { - tdsWriter.writeRPCUUID(name, uuidValue, isOutParam); - } - void execute(DTV dtv, byte[] byteArrayValue) throws SQLServerException { if (null != cryptoMeta) { tdsWriter.writeRPCNameValType(name, isOutParam, TDSType.BIGVARBINARY); @@ -1547,11 +1537,8 @@ final void executeOp(DTVExecuteOp op) throws SQLServerException { case VARCHAR: case LONGVARCHAR: case CLOB: - op.execute(this, (byte[]) null); - break; - case GUID: - op.execute(this, (UUID) null); + op.execute(this, (byte[]) null); break; case TINYINT: @@ -1632,7 +1619,7 @@ final void executeOp(DTVExecuteOp op) throws SQLServerException { byte[] bArray = Util.asGuidByteArray((UUID) value); op.execute(this, bArray); } else { - op.execute(this, UUID.fromString(String.valueOf(value))); + op.execute(this, String.valueOf(value)); } } else if (JDBCType.SQL_VARIANT == jdbcType) { op.execute(this, String.valueOf(value)); @@ -2207,8 +2194,6 @@ void execute(DTV dtv, Short shortValue) throws SQLServerException {} void execute(DTV dtv, Boolean booleanValue) throws SQLServerException {} - void execute(DTV dtv, UUID uuidValue) throws SQLServerException {} - void execute(DTV dtv, byte[] byteArrayValue) throws SQLServerException {} void execute(DTV dtv, Blob blobValue) throws SQLServerException { diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/GuidTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/GuidTest.java deleted file mode 100644 index e8b5c157b..000000000 --- a/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/GuidTest.java +++ /dev/null @@ -1,97 +0,0 @@ -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); - } - } - } - -} From ed7d2c87c43deb84b5c9a77e12f95408d24f8f1e Mon Sep 17 00:00:00 2001 From: lilgreenbird Date: Tue, 26 Mar 2024 11:49:40 -0700 Subject: [PATCH 28/47] Add test tag for Azure Synapse and fixed formatting (#2367) --- .../sqlserver/jdbc/ISQLServerConnection.java | 1 + .../jdbc/exception/ChainedExceptionTest.java | 25 +- .../messageHandler/MessageHandlerTest.java | 259 ++++++++---------- 3 files changed, 133 insertions(+), 152 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java index e0e4eebbf..2479e5063 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java @@ -498,6 +498,7 @@ CallableStatement prepareCall(String sql, int nType, int nConcur, int nHold, * message handler * * @see ISQLServerMessageHandler#messageHandler(ISQLServerMessage) + * @return ISQLServerMessageHandler */ ISQLServerMessageHandler setServerMessageHandler(ISQLServerMessageHandler messageHandler); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/exception/ChainedExceptionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/exception/ChainedExceptionTest.java index 9e6b1d4b7..4c05d2fb7 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/exception/ChainedExceptionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/exception/ChainedExceptionTest.java @@ -12,12 +12,14 @@ 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) @@ -29,16 +31,17 @@ public static void setupTests() throws Exception { } @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"); + // 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")); @@ -46,18 +49,18 @@ public void testTwoExceptions() throws Exception { } catch (SQLException ex) { // Check the SQLException and the chain - int exCount = 0; + int exCount = 0; int firstMsgNum = ex.getErrorCode(); - int lastMsgNum = -1; + int lastMsgNum = -1; while (ex != null) { - exCount++; + exCount++; lastMsgNum = ex.getErrorCode(); - ex = ex.getNextException(); + ex = ex.getNextException(); } - + // Exception Count should be: 2 assertEquals(2, exCount, "Number of SQLExceptions in the SQLException chain"); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/messageHandler/MessageHandlerTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/messageHandler/MessageHandlerTest.java index 94042a7d8..b9d52a681 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/messageHandler/MessageHandlerTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/messageHandler/MessageHandlerTest.java @@ -8,6 +8,7 @@ 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; @@ -29,6 +30,7 @@ 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) @@ -39,45 +41,44 @@ 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) + * + * @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) - { + private static int getWarningCount(String str, SQLWarning sqlw) { int count = 0; - while(sqlw != null) { + while (sqlw != null) { count++; - //System.out.println("DEBUG: getWarningCount(): [" + str + "] SQLWarning: Error=" + sqlw.getErrorCode() + ", Severity=" + ((SQLServerWarning)sqlw).getSQLServerError().getErrorSeverity() + ", Text=|" + sqlw.getMessage() + "|."); + // 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 *

    - *
  • Insert duplicate row -- Mapped to Info Message
  • - *
  • Drop table that do not exist -- Mapped to ignore
  • + *
  • Insert duplicate row -- Mapped to Info Message
  • + *
  • Drop table that do not exist -- Mapped to ignore
  • *
*/ @Test + @Tag(Constants.xAzureSQLDW) public void testMsgHandlerWithStatement() throws Exception { try (SQLServerConnection conn = getConnection()) { - class TestMsgHandler implements ISQLServerMessageHandler - { - int numOfCalls = 0; + class TestMsgHandler implements ISQLServerMessageHandler { + int numOfCalls = 0; int numOfDowngrades = 0; - int numOfDiscards = 0; + int numOfDiscards = 0; @Override - public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) - { + public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) { numOfCalls++; ISQLServerMessage retObj = srvErrorOrWarning; @@ -111,34 +112,39 @@ public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) TestMsgHandler testMsgHandler = new TestMsgHandler(); // Create a massage handler - conn.setServerMessageHandler(testMsgHandler); + conn.setServerMessageHandler(testMsgHandler); - try (Statement stmnt = conn.createStatement()) { + 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."); + 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."); + 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."); + 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."); + 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."); + 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."); + 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."); @@ -151,32 +157,29 @@ public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) } } catch (SQLException ex) { - fail(TestResource.getResource("R_unexpectedErrorMessage")); + fail(TestResource.getResource("R_unexpectedErrorMessage") + ex.getMessage()); } } - - /** * Test message handler with PreparedStatement *
    - *
  • Insert duplicate row -- Mapped to Info Message
  • - *
  • Drop table that do not exist -- Mapped to ignore
  • + *
  • Insert duplicate row -- Mapped to Info Message
  • + *
  • Drop table that do not exist -- Mapped to ignore
  • *
*/ @Test + @Tag(Constants.xAzureSQLDW) public void testMsgHandlerWithPreparedStatement() throws Exception { try (SQLServerConnection conn = getConnection()) { - class TestMsgHandler implements ISQLServerMessageHandler - { - int numOfCalls = 0; + class TestMsgHandler implements ISQLServerMessageHandler { + int numOfCalls = 0; int numOfDowngrades = 0; - int numOfDiscards = 0; + int numOfDiscards = 0; @Override - public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) - { + public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) { numOfCalls++; ISQLServerMessage retObj = srvErrorOrWarning; @@ -210,50 +213,51 @@ public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) TestMsgHandler testMsgHandler = new TestMsgHandler(); // Create a massage handler - conn.setServerMessageHandler(testMsgHandler); + conn.setServerMessageHandler(testMsgHandler); - try (Statement stmnt = conn.createStatement()) { + 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(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()) { + 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(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(?, ?)")) { + 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(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(?, ?)")) { + 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."); + 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()) { + 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(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()) { + 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(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."); @@ -264,32 +268,29 @@ public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) assertEquals(2, testMsgHandler.numOfDiscards, "Number of message Discards in the message handler."); } catch (SQLException ex) { - fail(TestResource.getResource("R_unexpectedErrorMessage")); + fail(TestResource.getResource("R_unexpectedErrorMessage") + ex.getMessage()); } } - - /** * Test message handler with CallableStatement *
    - *
  • Insert duplicate row -- Mapped to Info Message
  • - *
  • Drop table that do not exist -- Mapped to ignore
  • + *
  • Insert duplicate row -- Mapped to Info Message
  • + *
  • Drop table that do not exist -- Mapped to ignore
  • *
*/ @Test + @Tag(Constants.xAzureSQLDW) public void testMsgHandlerWithCallableStatement() throws Exception { try (SQLServerConnection conn = getConnection()) { - class TestMsgHandler implements ISQLServerMessageHandler - { - int numOfCalls = 0; + class TestMsgHandler implements ISQLServerMessageHandler { + int numOfCalls = 0; int numOfDowngrades = 0; - int numOfDiscards = 0; + int numOfDiscards = 0; @Override - public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) - { + public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) { numOfCalls++; ISQLServerMessage retObj = srvErrorOrWarning; @@ -323,59 +324,47 @@ public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) TestMsgHandler testMsgHandler = new TestMsgHandler(); // Create a massage handler - conn.setServerMessageHandler(testMsgHandler); + 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" + 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" + + " 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" + + " \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" - ; + + " DROP TABLE #msghandler_tmp_table \n" + " \n" + " RETURN 1 \n" + "END \n"; // Create the proc - try (Statement stmnt = conn.createStatement()) { + 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."); + 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(?) }")) { + 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); + 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, 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."); + 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 @@ -388,12 +377,9 @@ public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) assertEquals(2, testMsgHandler.numOfDiscards, "Number of message Discards in the message handler."); } catch (SQLException ex) { - fail(TestResource.getResource("R_unexpectedErrorMessage")); + fail(TestResource.getResource("R_unexpectedErrorMessage") + ex.getMessage()); } } - - - /** * Test message handler with CallableStatement -- and "feedback" messages @@ -401,21 +387,20 @@ public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) * 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 - { + class TestMsgHandler implements ISQLServerMessageHandler { int numOfCalls = 0; Map feedbackMsgTs = new LinkedHashMap<>(); @Override - public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) - { + public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) { numOfCalls++; if (50_000 == srvErrorOrWarning.getErrorNumber()) { - //System.out.println("DEBUG: testMsgHandlerWithProcedureFeedback.messageHandler(): FEEDBACK: " + srvErrorOrWarning.getErrorMessage()); + // System.out.println("DEBUG: testMsgHandlerWithProcedureFeedback.messageHandler(): FEEDBACK: " + srvErrorOrWarning.getErrorMessage()); // Remember when the message was received feedbackMsgTs.put(srvErrorOrWarning.getErrorMessage(), System.currentTimeMillis()); } @@ -426,48 +411,40 @@ public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) TestMsgHandler testMsgHandler = new TestMsgHandler(); // Create a massage handler - conn.setServerMessageHandler(testMsgHandler); + 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" - ; + 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()) { + 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."); + 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 }")) { + 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."); + 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: # @@ -483,15 +460,15 @@ public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) 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() + "|."); + 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")); + fail(TestResource.getResource("R_unexpectedErrorMessage") + ex.getMessage()); } } - } From 7e0dcb39bcb3ff737c13abf7b1359d3b65ada6f7 Mon Sep 17 00:00:00 2001 From: Terry Chow <32403408+tkyc@users.noreply.github.com> Date: Tue, 26 Mar 2024 12:43:56 -0700 Subject: [PATCH 29/47] socketTimeout should reset to original value after a successful connection open (#2355) --- .../java/com/microsoft/sqlserver/jdbc/IOBuffer.java | 4 ++++ .../sqlserver/jdbc/SQLServerConnection.java | 6 ++++++ .../sqlserver/jdbc/connection/TimeoutTest.java | 12 ++++++++++++ 3 files changed, 22 insertions(+) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index 3a5db2bf8..afde39106 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -2366,6 +2366,10 @@ final int getNetworkTimeout() throws IOException { 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/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 8012bea5c..7a72a0475 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -3210,9 +3210,15 @@ else if (0 == requestedPacketSize) 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) 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 68fbfc8df..b9cdd4cda 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,18 @@ public void testConnectRetryTimeout() { "total time: " + totalTime + " interval: " + TimeUnit.SECONDS.toMillis(interval)); } + @Test + 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() { From f626adff039afd3e0b4df17d6f89d0aedc20b53a Mon Sep 17 00:00:00 2001 From: lilgreenbird Date: Tue, 26 Mar 2024 18:56:50 -0700 Subject: [PATCH 30/47] Fix ClassLoader leak of ActivityCorrelator ThreadLocal (#2366) --- .../sqlserver/jdbc/ActivityCorrelator.java | 34 ++++++++++++++----- .../sqlserver/jdbc/SQLServerConnection.java | 4 +++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ActivityCorrelator.java b/src/main/java/com/microsoft/sqlserver/jdbc/ActivityCorrelator.java index 974ac3c47..072d34f51 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 t_ActivityId = new ThreadLocal() { - @Override - protected ActivityId initialValue() { - return new ActivityId(); - } - }; + private static Map activityIdMap = new ConcurrentHashMap(); 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 @@ static ActivityId getNext() { * 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/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 7a72a0475..127965397 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -4791,6 +4791,8 @@ private void clearConnectionResources() { // Clean-up queue etc. related to batching of prepared statement discard actions (sp_unprepare). cleanupPreparedStatementDiscardActions(); + + ActivityCorrelator.cleanupActivityId(); } /** @@ -4812,6 +4814,8 @@ final void poolCloseEventNotify() throws SQLServerException { } notifyPooledConnection(null); + ActivityCorrelator.cleanupActivityId(); + if (connectionlogger.isLoggable(Level.FINER)) { connectionlogger.finer(toString() + " Connection closed and returned to connection pool"); } From 539b117b81d28ef63a9ac8b2e09ae9a2263892c3 Mon Sep 17 00:00:00 2001 From: Terry Chow <32403408+tkyc@users.noreply.github.com> Date: Thu, 28 Mar 2024 11:14:04 -0700 Subject: [PATCH 31/47] Excluded DW (#2372) --- .../com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java | 1 + 1 file changed, 1 insertion(+) 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 b9cdd4cda..268fda736 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java @@ -314,6 +314,7 @@ public void testConnectRetryTimeout() { } @Test + @Tag(Constants.xAzureSQLDW) public void testSocketTimeoutBoundedByLoginTimeoutReset() throws Exception { try (Connection con = PrepUtil.getConnection(connectionString + ";socketTimeout=90000;loginTimeout=10;"); Statement stmt = con.createStatement()) { From 662a26620064bcbf1db30cec8488bd7e0ed7b860 Mon Sep 17 00:00:00 2001 From: funkyjive Date: Mon, 1 Apr 2024 15:32:33 -0600 Subject: [PATCH 32/47] Finish support for RFC4180 for CSV bulk insert operations (#2338) --- .../jdbc/SQLServerBulkCSVFileRecord.java | 43 ++++++++++++++++++- .../jdbc/bulkCopy/BulkCopyCSVTest.java | 3 +- .../BulkCopyCSVTestInputDelimiterEscape.csv | 4 ++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java index 195b197e0..25f99214b 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 @@ private void initFileReader(InputStreamReader sr, String encoding, String demlim } } + /* + * 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 @@ else if ((null != columnNames) && (columnNames.length >= positionInSource)) @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/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyCSVTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyCSVTest.java index 48e28b712..97b0e3f92 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 void testEscapeColumnDelimitersCSV() throws Exception { /* * 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 void testEscapeColumnDelimitersCSV() throws Exception { 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/resources/BulkCopyCSVTestInputDelimiterEscape.csv b/src/test/resources/BulkCopyCSVTestInputDelimiterEscape.csv index 10afd5d84..9425b0a1d 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"""@ From 30f2ae0dee085b4ec35efbd20a089dab3a7a48df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20de=20Launois?= Date: Tue, 2 Apr 2024 21:30:43 +0200 Subject: [PATCH 33/47] Add support for TDSType.GUID (fix) (#1582) (#2370) --- .../microsoft/sqlserver/jdbc/IOBuffer.java | 27 +++++ .../microsoft/sqlserver/jdbc/Parameter.java | 5 + .../com/microsoft/sqlserver/jdbc/dtv.java | 24 ++++- .../sqlserver/jdbc/datatypes/GuidTest.java | 99 +++++++++++++++++++ 4 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/microsoft/sqlserver/jdbc/datatypes/GuidTest.java diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index afde39106..770cbbae3 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -57,6 +57,7 @@ import java.util.Set; import java.util.SimpleTimeZone; import java.util.TimeZone; +import java.util.UUID; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; @@ -4788,6 +4789,32 @@ void writeRPCBigDecimal(String sName, BigDecimal bdValue, int nScale, boolean bO writeBytes(val, 0, val.length); } + /** + * Append a UUID in RPC transmission format. + * + * @param sName + * the optional parameter name + * @param uuidValue + * the data value + * @param bOut + * boolean true if the data value is being registered as an output parameter + */ + void writeRPCUUID(String sName, UUID uuidValue, boolean bOut) throws SQLServerException { + writeRPCNameValType(sName, bOut, TDSType.GUID); + + if (uuidValue == null) { + writeByte((byte) 0); + writeByte((byte) 0); + + } else { + writeByte((byte) 0x10); // maximum length = 16 + writeByte((byte) 0x10); // length = 16 + + byte[] val = Util.asGuidByteArray(uuidValue); + writeBytes(val, 0, val.length); + } + } + /** * Appends a standard v*max header for RPC parameter transmission. * diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java b/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java index 4894c45f2..6b61cf53e 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java @@ -22,6 +22,7 @@ import java.time.OffsetTime; import java.util.Calendar; import java.util.Locale; +import java.util.UUID; /** @@ -1125,6 +1126,10 @@ void execute(DTV dtv, Boolean booleanValue) throws SQLServerException { setTypeDefinition(dtv); } + void execute(DTV dtv, UUID uuidValue) throws SQLServerException { + setTypeDefinition(dtv); + } + void execute(DTV dtv, byte[] byteArrayValue) throws SQLServerException { // exclude JDBC typecasting for Geometry/Geography as these datatypes don't have a size limit. if (null != byteArrayValue && byteArrayValue.length > DataTypes.SHORT_VARTYPE_MAX_BYTES diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java index c03d67eb0..3c08a80cd 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java @@ -99,6 +99,8 @@ abstract class DTVExecuteOp { abstract void execute(DTV dtv, Boolean booleanValue) throws SQLServerException; + abstract void execute(DTV dtv, UUID uuidValue) throws SQLServerException; + abstract void execute(DTV dtv, byte[] byteArrayValue) throws SQLServerException; abstract void execute(DTV dtv, Blob blobValue) throws SQLServerException; @@ -291,7 +293,11 @@ final class SendByRPCOp extends DTVExecuteOp { } void execute(DTV dtv, String strValue) throws SQLServerException { - tdsWriter.writeRPCStringUnicode(name, strValue, isOutParam, collation, dtv.isNonPLP); + if (dtv.getJdbcType() == JDBCType.GUID) { + tdsWriter.writeRPCUUID(name, UUID.fromString(strValue), isOutParam); + } else { + tdsWriter.writeRPCStringUnicode(name, strValue, isOutParam, collation, dtv.isNonPLP); + } } void execute(DTV dtv, Clob clobValue) throws SQLServerException { @@ -1126,6 +1132,10 @@ void execute(DTV dtv, Boolean booleanValue) throws SQLServerException { tdsWriter.writeRPCBit(name, booleanValue, isOutParam); } + void execute(DTV dtv, UUID uuidValue) throws SQLServerException { + tdsWriter.writeRPCUUID(name, uuidValue, isOutParam); + } + void execute(DTV dtv, byte[] byteArrayValue) throws SQLServerException { if (null != cryptoMeta) { tdsWriter.writeRPCNameValType(name, isOutParam, TDSType.BIGVARBINARY); @@ -1537,10 +1547,16 @@ final void executeOp(DTVExecuteOp op) throws SQLServerException { case VARCHAR: case LONGVARCHAR: case CLOB: - case GUID: op.execute(this, (byte[]) null); break; + case GUID: + if (null != cryptoMeta) + op.execute(this, (byte[]) null); + else + op.execute(this, (UUID) null); + break; + case TINYINT: if (null != cryptoMeta) op.execute(this, (byte[]) null); @@ -1619,7 +1635,7 @@ final void executeOp(DTVExecuteOp op) throws SQLServerException { byte[] bArray = Util.asGuidByteArray((UUID) value); op.execute(this, bArray); } else { - op.execute(this, String.valueOf(value)); + op.execute(this, UUID.fromString(String.valueOf(value))); } } else if (JDBCType.SQL_VARIANT == jdbcType) { op.execute(this, String.valueOf(value)); @@ -2194,6 +2210,8 @@ void execute(DTV dtv, Short shortValue) throws SQLServerException {} void execute(DTV dtv, Boolean booleanValue) throws SQLServerException {} + void execute(DTV dtv, UUID uuidValue) throws SQLServerException {} + void execute(DTV dtv, byte[] byteArrayValue) throws SQLServerException {} void execute(DTV dtv, Blob blobValue) throws SQLServerException { diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/GuidTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/GuidTest.java new file mode 100644 index 000000000..2e0968ebe --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/GuidTest.java @@ -0,0 +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.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); + } + } + } + +} From 7c78c482f636970b24b369899b1a5ff925b2e533 Mon Sep 17 00:00:00 2001 From: lilgreenbird Date: Tue, 2 Apr 2024 12:31:14 -0700 Subject: [PATCH 34/47] clear pstmt cache (#2361) --- .../sqlserver/jdbc/SQLServerConnection.java | 5 ++ .../jdbc/connection/PoolingTest.java | 67 ++++++++++++++++++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 127965397..00fbf59e0 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -1797,6 +1797,11 @@ final Connection getConnection() { final void resetPooledConnection() { tdsChannel.resetPooledConnection(); initResettableValues(); + + // reset prepared statement handle cache + if (null != preparedStatementHandleCache) { + preparedStatementHandleCache.clear(); + } } /** 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 a23d6151f..d99d846ef 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.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 @@ * 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 void testApacheDBCP() throws SQLException { } } + /** + * 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 * From bea8daa4ab132445b4dd2427d9f9426b8bcabf23 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Tue, 2 Apr 2024 14:01:55 -0700 Subject: [PATCH 35/47] Check if TDSCommand counter is null before incrementing. (#2368) * Null check for getCounter * Review fixes * Added trailing comma to reduce code diff --- src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java | 9 +++++++++ .../com/microsoft/sqlserver/jdbc/SQLServerResource.java | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index 770cbbae3..923bae7b3 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -6943,6 +6943,15 @@ final boolean readPacket() throws SQLServerException { // if messageType is RPC or QUERY, then increment Counter's state if (tdsChannel.getWriter().checkIfTdsMessageTypeIsBatchOrRPC() && null != command) { + if (logger.isLoggable(Level.FINER)) { + logger.warning(toString() + ": increasing state of counter for TDS Command: " + command.toString()); + } + + if (null == command.getCounter()) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_NullValue")); + Object[] msgArgs1 = {"TDS command counter"}; + throw new SQLServerException(form.format(msgArgs1), null); + } command.getCounter().increaseCounter(packetLength); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index 6cd502b72..28a1b219a 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -535,7 +535,7 @@ protected Object[][] getContents() { {"R_crCommandCannotTimeOut", "Request failed to time out and SQLServerConnection does not exist"}, {"R_InvalidIPAddressPreference", "IP address preference {0} is not valid."}, {"R_UnableLoadAuthDll", "Unable to load authentication DLL {0}"}, - {"R_illegalArgumentTrustManager", "Interal error. Peer certificate chain or key exchange algorithem can not be null or empty."}, + {"R_illegalArgumentTrustManager", "Internal error. Peer certificate chain or key exchange algorithm can not be null or empty."}, {"R_serverCertExpired", "Server Certificate has expired: {0}: {1}"}, {"R_serverCertNotYetValid", "Server Certificate is not yet valid: {0}: {1}"}, {"R_serverCertError", "Error validating Server Certificate: {0}: \n{1}:\n{2}."}, From 1f0d85a266abc012edaf2a53c69db3b6c1e146c2 Mon Sep 17 00:00:00 2001 From: Terry Chow <32403408+tkyc@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:26:53 -0700 Subject: [PATCH 36/47] Escape schema for getProcedures and getProcedureColumns in SQLServerDatabaseMetaData (#2369) * Initial changes * Added test * Code review changes * Code review changes p2 --- .../jdbc/SQLServerDatabaseMetaData.java | 6 ++-- .../microsoft/sqlserver/jdbc/TestUtils.java | 10 ++++++ .../DatabaseMetaDataTest.java | 32 +++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java index 5c23a4777..12cc843b7 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java @@ -1422,10 +1422,10 @@ public java.sql.ResultSet getProcedureColumns(String catalog, String schema, Str String[] arguments = new String[5]; - // proc name supports escaping + // proc, schema and col name supports escaping proc = escapeIDName(proc); arguments[0] = proc; - arguments[1] = schema; + arguments[1] = escapeIDName(schema); arguments[2] = catalog; // col name supports escaping col = escapeIDName(col); @@ -1466,7 +1466,7 @@ public java.sql.ResultSet getProcedures(String catalog, String schema, */ String[] arguments = new String[3]; arguments[0] = escapeIDName(proc); - arguments[1] = schema; + arguments[1] = escapeIDName(schema); arguments[2] = catalog; return getResultSetWithProvidedColumnNames(catalog, CallableHandles.SP_STORED_PROCEDURES, arguments, getProceduresColumnNames); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java index cd20087c1..7a9ea31d4 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java @@ -419,6 +419,16 @@ public static void dropTableIfExists(String tableName, java.sql.Statement stmt) dropObjectIfExists(tableName, "U", stmt); } + public static void dropTableWithSchemaIfExists(String tableNameWithSchema, java.sql.Statement stmt) throws SQLException { + stmt.execute("IF OBJECT_ID('" + tableNameWithSchema + "', 'U') IS NOT NULL DROP TABLE " + tableNameWithSchema + ";"); + } + + + public static void dropProcedureWithSchemaIfExists(String procedureWithSchema, java.sql.Statement stmt) throws SQLException { + stmt.execute("IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'" + + procedureWithSchema + "') AND type in (N'P', N'PC')) DROP PROCEDURE " + procedureWithSchema + ";"); + } + /** * Deletes the contents of a table. * diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java index 999ccdc1e..dd9a96e2b 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java @@ -59,8 +59,13 @@ @RunWith(JUnitPlatform.class) public class DatabaseMetaDataTest extends AbstractTest { + private static final String uuid = UUID.randomUUID().toString().replaceAll("-", ""); private static final String tableName = RandomUtil.getIdentifier("DBMetadataTable"); private static final String functionName = RandomUtil.getIdentifier("DBMetadataFunction"); + private static final String schema = "schema_demo" + uuid; + private static final String escapedSchema = "schema\\_demo" + uuid; + private static final String tableNameWithSchema = schema + ".resource"; + private static final String sprocWithSchema = schema + ".updateresource"; private static Map getColumnsDWColumns = null; private static Map getImportedKeysDWColumns = null; private static final String TABLE_CAT = "TABLE_CAT"; @@ -991,6 +996,25 @@ public void testValidateColumnMetadata() throws SQLException { } } + @Test + public void shouldEscapeSchemaName() throws SQLException { + try (Connection con = getConnection()) { + DatabaseMetaData md = con.getMetaData(); + try (ResultSet procedures = md.getProcedures( + null, escapedSchema, "updateresource")) { + if (!procedures.next()) { + fail("Escaped schema pattern did not succeed. No results found."); + } + } + + try (ResultSet columns = md.getProcedureColumns(null, escapedSchema, "updateresource", null)) { + if (!columns.next()) { + fail("Escaped schema pattern did not succeed. No results found."); + } + } + } + } + @BeforeAll public static void setupTable() throws Exception { setConnection(); @@ -1000,6 +1024,11 @@ public static void setupTable() throws Exception { + " ([col_1] int NOT NULL, [col%2] varchar(200), [col[3] decimal(15,2))"); stmt.execute("CREATE FUNCTION " + AbstractSQLGenerator.escapeIdentifier(functionName) + " (@p1 INT, @p2 INT) RETURNS INT AS BEGIN DECLARE @result INT; SET @result = @p1 + @p2; RETURN @result; END"); + stmt.execute("CREATE SCHEMA " + schema); + stmt.execute("CREATE TABLE " + tableNameWithSchema + " (id UNIQUEIDENTIFIER, name NVARCHAR(400));"); + stmt.execute("CREATE PROCEDURE " + sprocWithSchema + "(@id UNIQUEIDENTIFIER, @name VARCHAR(400)) AS " + + "BEGIN SET TRANSACTION ISOLATION LEVEL SERIALIZABLE BEGIN TRANSACTION UPDATE " + + tableNameWithSchema + " SET name = @name WHERE id = @id COMMIT END"); } } @@ -1008,6 +1037,9 @@ public static void terminate() throws SQLException { try (Statement stmt = connection.createStatement()) { TestUtils.dropTableIfExists(tableName, stmt); TestUtils.dropFunctionIfExists(functionName, stmt); + TestUtils.dropTableWithSchemaIfExists(tableNameWithSchema, stmt); + TestUtils.dropProcedureWithSchemaIfExists(sprocWithSchema, stmt); + TestUtils.dropSchemaIfExists(schema, stmt); } } } From 9de1a5db48c75996373b64ff1c263043bdcfc819 Mon Sep 17 00:00:00 2001 From: Terry Chow <32403408+tkyc@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:50:39 -0700 Subject: [PATCH 37/47] Warning log spam correction and test exclusion (#2377) * Removed warning message * Excluded test from DW * Excluded test from DW p2 --- .../microsoft/sqlserver/jdbc/IOBuffer.java | 4 ---- .../DatabaseMetaDataTest.java | 20 ++++++++++++++----- .../BatchExecutionWithBulkCopyTest.java | 1 + 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index 923bae7b3..0f5a107e1 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -6943,10 +6943,6 @@ final boolean readPacket() throws SQLServerException { // if messageType is RPC or QUERY, then increment Counter's state if (tdsChannel.getWriter().checkIfTdsMessageTypeIsBatchOrRPC() && null != command) { - if (logger.isLoggable(Level.FINER)) { - logger.warning(toString() + ": increasing state of counter for TDS Command: " + command.toString()); - } - if (null == command.getCounter()) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_NullValue")); Object[] msgArgs1 = {"TDS command counter"}; diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java index dd9a96e2b..fee7e274f 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java @@ -997,7 +997,16 @@ public void testValidateColumnMetadata() throws SQLException { } @Test + @Tag(Constants.xAzureSQLDW) public void shouldEscapeSchemaName() throws SQLException { + try (Statement stmt = connection.createStatement()) { + stmt.execute("CREATE SCHEMA " + schema); + stmt.execute("CREATE TABLE " + tableNameWithSchema + " (id UNIQUEIDENTIFIER, name NVARCHAR(400));"); + stmt.execute("CREATE PROCEDURE " + sprocWithSchema + "(@id UNIQUEIDENTIFIER, @name VARCHAR(400)) AS " + + "BEGIN SET TRANSACTION ISOLATION LEVEL SERIALIZABLE BEGIN TRANSACTION UPDATE " + + tableNameWithSchema + " SET name = @name WHERE id = @id COMMIT END"); + } + try (Connection con = getConnection()) { DatabaseMetaData md = con.getMetaData(); try (ResultSet procedures = md.getProcedures( @@ -1013,6 +1022,12 @@ public void shouldEscapeSchemaName() throws SQLException { } } } + + try (Statement stmt = connection.createStatement()) { + TestUtils.dropTableWithSchemaIfExists(tableNameWithSchema, stmt); + TestUtils.dropProcedureWithSchemaIfExists(sprocWithSchema, stmt); + TestUtils.dropSchemaIfExists(schema, stmt); + } } @BeforeAll @@ -1024,11 +1039,6 @@ public static void setupTable() throws Exception { + " ([col_1] int NOT NULL, [col%2] varchar(200), [col[3] decimal(15,2))"); stmt.execute("CREATE FUNCTION " + AbstractSQLGenerator.escapeIdentifier(functionName) + " (@p1 INT, @p2 INT) RETURNS INT AS BEGIN DECLARE @result INT; SET @result = @p1 + @p2; RETURN @result; END"); - stmt.execute("CREATE SCHEMA " + schema); - stmt.execute("CREATE TABLE " + tableNameWithSchema + " (id UNIQUEIDENTIFIER, name NVARCHAR(400));"); - stmt.execute("CREATE PROCEDURE " + sprocWithSchema + "(@id UNIQUEIDENTIFIER, @name VARCHAR(400)) AS " + - "BEGIN SET TRANSACTION ISOLATION LEVEL SERIALIZABLE BEGIN TRANSACTION UPDATE " - + tableNameWithSchema + " SET name = @name WHERE id = @id COMMIT END"); } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java index 4d263d007..c1917fd68 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java @@ -335,6 +335,7 @@ public void testMixColumns() throws Exception { } @Test + @Tag((Constants.xAzureSQLDW)) public void testNullGuid() throws Exception { String valid = "insert into " + AbstractSQLGenerator.escapeIdentifier(tableName) + " (c24) values (?)"; try (Connection connection = PrepUtil.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); From 51ca5a0d3a0eff6890b08be5c77bbaad61322d5c Mon Sep 17 00:00:00 2001 From: lukasaignerrsg <164002570+lukasaignerrsg@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:58:29 +0200 Subject: [PATCH 38/47] Clear prepared statement handle before reconnect (#2364) --- .../sqlserver/jdbc/ReconnectListener.java | 15 +++ .../sqlserver/jdbc/SQLServerConnection.java | 15 +++ .../jdbc/SQLServerPreparedStatement.java | 19 ++++ .../RequestBoundaryMethodsTest.java | 2 + .../jdbc/resiliency/BasicConnectionTest.java | 99 +++++++++++++++++++ 5 files changed, 150 insertions(+) create mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/ReconnectListener.java diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ReconnectListener.java b/src/main/java/com/microsoft/sqlserver/jdbc/ReconnectListener.java new file mode 100644 index 000000000..c56045dfd --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ReconnectListener.java @@ -0,0 +1,15 @@ +/* + * 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; + +/** + * This functional interface represents a listener which is called before a reconnect of {@link SQLServerConnection}. + */ +@FunctionalInterface +public interface ReconnectListener { + + void beforeReconnect(); + +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 00fbf59e0..48275d2aa 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -1759,6 +1759,19 @@ SQLServerPooledConnection getPooledConnectionParent() { return pooledConnectionParent; } + /** + * List of listeners which are called before reconnecting. + */ + private List reconnectListeners = new ArrayList<>(); + + public void registerBeforeReconnectListener(ReconnectListener reconnectListener) { + reconnectListeners.add(reconnectListener); + } + + public void removeBeforeReconnectListener(ReconnectListener reconnectListener) { + reconnectListeners.remove(reconnectListener); + } + SQLServerConnection(String parentInfo) { int connectionID = nextConnectionID(); // sequential connection id traceID = "ConnectionID:" + connectionID; @@ -4345,6 +4358,8 @@ boolean executeCommand(TDSCommand newCommand) throws SQLServerException { preparedStatementHandleCache.clear(); } + this.reconnectListeners.forEach(ReconnectListener::beforeReconnect); + if (loggerResiliency.isLoggable(Level.FINE)) { loggerResiliency.fine(toString() + " Idle connection resiliency - starting idle connection resiliency reconnect."); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index 4274e6913..5f1766d94 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -234,6 +234,12 @@ private boolean resetPrepStmtHandle(boolean discardCurrentCacheItem) { */ private Vector cryptoMetaBatch = new Vector<>(); + /** + * Listener to clear the {@link SQLServerPreparedStatement#prepStmtHandle} and + * {@link SQLServerPreparedStatement#cachedPreparedStatementHandle} before reconnecting. + */ + private ReconnectListener clearPrepStmtHandleOnReconnectListener; + /** * Constructs a SQLServerPreparedStatement. * @@ -254,6 +260,9 @@ private boolean resetPrepStmtHandle(boolean discardCurrentCacheItem) { SQLServerStatementColumnEncryptionSetting stmtColEncSetting) throws SQLServerException { super(conn, nRSType, nRSConcur, stmtColEncSetting); + clearPrepStmtHandleOnReconnectListener = this::clearPrepStmtHandle; + connection.registerBeforeReconnectListener(clearPrepStmtHandleOnReconnectListener); + if (null == sql) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_NullValue")); Object[] msgArgs1 = {"Statement SQL"}; @@ -291,6 +300,8 @@ private boolean resetPrepStmtHandle(boolean discardCurrentCacheItem) { * Closes the prepared statement's prepared handle. */ private void closePreparedHandle() { + connection.removeBeforeReconnectListener(clearPrepStmtHandleOnReconnectListener); + if (!hasPreparedStatementHandle()) return; @@ -3586,4 +3597,12 @@ public void addBatch(String sql) throws SQLServerException { Object[] msgArgs = {"addBatch()"}; throw new SQLServerException(this, form.format(msgArgs), null, 0, false); } + + private void clearPrepStmtHandle() { + prepStmtHandle = 0; + cachedPreparedStatementHandle = null; + if (getStatementLogger().isLoggable(Level.FINER)) { + getStatementLogger().finer(toString() + " cleared cachedPrepStmtHandle!"); + } + } } 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 926b6a766..dbdec02cf 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java @@ -529,6 +529,8 @@ private List getVerifiedMethodNames() { verifiedMethodNames.add("setUseFlexibleCallableStatements"); verifiedMethodNames.add("getCalcBigDecimalPrecision"); verifiedMethodNames.add("setCalcBigDecimalPrecision"); + verifiedMethodNames.add("registerBeforeReconnectListener"); + verifiedMethodNames.add("removeBeforeReconnectListener"); return verifiedMethodNames; } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java index 3712e9d1d..7fa028c2e 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java @@ -6,15 +6,20 @@ package com.microsoft.sqlserver.jdbc.resiliency; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.lang.reflect.Field; import java.sql.Connection; import java.sql.DriverManager; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; import java.util.UUID; import javax.sql.PooledConnection; @@ -367,6 +372,100 @@ public void testPreparedStatementCacheShouldBeCleared() throws SQLException { } } + @Test + public void testPreparedStatementHandleOfStatementShouldBeCleared() throws SQLException { + try (SQLServerConnection con = (SQLServerConnection) ResiliencyUtils.getConnection(connectionString)) { + int cacheSize = 2; + String query = String.format("/*testPreparedStatementHandleOfStatementShouldBeCleared%s*/SELECT 1; -- ", + UUID.randomUUID().toString()); + + // enable caching + con.setDisableStatementPooling(false); + con.setStatementPoolingCacheSize(cacheSize); + con.setServerPreparedStatementDiscardThreshold(cacheSize); + + List statements = new LinkedList<>(); + + // add statements to fill cache + for (int i = 0; i < cacheSize + 1; ++i) { + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) con.prepareStatement(query + i); + pstmt.execute(); + pstmt.execute(); + pstmt.execute(); + pstmt.getMoreResults(); + statements.add(pstmt); + } + + // handle of the prepared statement should be set + assertNotEquals(0, statements.get(1).getPreparedStatementHandle()); + + ResiliencyUtils.killConnection(con, connectionString, 1); + + // call first statement to trigger reconnect + statements.get(0).execute(); + + // handle of the other statements should be cleared after reconnect + assertEquals(0, statements.get(1).getPreparedStatementHandle()); + } + } + + @Test + public void testPreparedStatementShouldNotUseWrongHandleAfterReconnect() throws SQLException { + try (SQLServerConnection con = (SQLServerConnection) ResiliencyUtils.getConnection(connectionString)) { + int cacheSize = 3; + String queryOne = "select * from sys.sysusers where name=?;"; + String queryTwo = "select * from sys.sysusers where name=? and uid=?;"; + String queryThree = "select * from sys.sysusers where name=? and uid=? and islogin=?"; + + String parameterOne = "name"; + int parameterUid = 0; + int parameterIsLogin = 0; + + // enable caching + con.setDisableStatementPooling(false); + con.setStatementPoolingCacheSize(cacheSize); + con.setServerPreparedStatementDiscardThreshold(cacheSize); + + List statements = new LinkedList<>(); + + PreparedStatement ps = con.prepareStatement(queryOne); + ps.setString(1, parameterOne); + statements.add(ps); + + ps = con.prepareStatement(queryTwo); + ps.setString(1, parameterOne); + ps.setInt(2, parameterUid); + statements.add(ps); + + ps = con.prepareStatement(queryThree); + ps.setString(1, parameterOne); + ps.setInt(2, parameterUid); + ps.setInt(3, parameterIsLogin); + statements.add(ps); + + // add new statements to fill cache + for (PreparedStatement preparedStatement : statements) { + preparedStatement.execute(); + preparedStatement.execute(); + preparedStatement.execute(); + preparedStatement.getMoreResults(); + } + + ResiliencyUtils.killConnection(con, connectionString, 1); + + // call statements in reversed order, in order to force the statement to use the wrong handle + // first execute triggers a reconnect + Collections.reverse(statements); + for (PreparedStatement preparedStatement : statements) { + preparedStatement.execute(); + preparedStatement.execute(); + preparedStatement.execute(); + preparedStatement.getMoreResults(); + } + } + } + + @Test public void testUnprocessedResponseCountSuccessfulIdleConnectionRecovery() throws SQLException { try (SQLServerConnection con = (SQLServerConnection) ResiliencyUtils.getConnection(connectionString)) { From c898a2753b60f43ebab25832f4c4a7d50a26f902 Mon Sep 17 00:00:00 2001 From: lilgreenbird Date: Wed, 3 Apr 2024 14:18:00 -0700 Subject: [PATCH 39/47] fixed formatting (#2378) --- .../com/microsoft/sqlserver/jdbc/IOBuffer.java | 4 ++-- .../jdbc/SQLServerConnectionPoolProxy.java | 4 ++-- .../sqlserver/jdbc/SQLServerException.java | 15 +++++++-------- .../sqlserver/jdbc/SQLServerStatement.java | 16 ++++++++-------- .../sqlserver/jdbc/SQLServerWarning.java | 16 +++++++--------- .../sqlserver/jdbc/SQLServerXAResource.java | 1 - .../microsoft/sqlserver/jdbc/TestResource.java | 3 +-- .../microsoft/sqlserver/jdbc/TestUtils.java | 14 ++++++++------ .../jdbc/bulkCopy/BulkCopyCSVTest.java | 3 ++- .../CallableStatementTest.java | 18 ++++++++++++------ .../databasemetadata/DatabaseMetaDataTest.java | 7 +++---- .../sqlserver/jdbc/datatypes/GuidTest.java | 5 +++-- .../jdbc/resiliency/BasicConnectionTest.java | 1 - 13 files changed, 55 insertions(+), 52 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index 0f5a107e1..4673a9e7c 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -4807,8 +4807,8 @@ void writeRPCUUID(String sName, UUID uuidValue, boolean bOut) throws SQLServerEx writeByte((byte) 0); } else { - writeByte((byte) 0x10); // maximum length = 16 - writeByte((byte) 0x10); // length = 16 + writeByte((byte) 0x10); // maximum length = 16 + writeByte((byte) 0x10); // length = 16 byte[] val = Util.asGuidByteArray(uuidValue); writeBytes(val, 0, val.length); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java index 5ed4b7b06..899f10595 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java @@ -696,10 +696,10 @@ public void setAccessTokenCallbackClass(String accessTokenCallbackClass) { public ISQLServerMessageHandler getServerMessageHandler() { return wrappedConnection.getServerMessageHandler(); } - + @Override public ISQLServerMessageHandler setServerMessageHandler(ISQLServerMessageHandler messageHandler) { - return wrappedConnection.setServerMessageHandler(messageHandler); + return wrappedConnection.setServerMessageHandler(messageHandler); } /** diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java index 77367708a..57387bb03 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java @@ -180,13 +180,12 @@ static String getErrString(String errCode) { } public SQLServerException(SQLServerError sqlServerError) { - super(sqlServerError.getErrorMessage(), - generateStateCode(null, sqlServerError.getErrorNumber(), sqlServerError.getErrorState()), - sqlServerError.getErrorNumber(), - null); + super(sqlServerError.getErrorMessage(), + generateStateCode(null, sqlServerError.getErrorNumber(), sqlServerError.getErrorState()), + sqlServerError.getErrorNumber(), null); this.sqlServerError = sqlServerError; - } + } /** * Constructs a new SQLServerException. @@ -278,10 +277,10 @@ static void makeFromDatabaseError(SQLServerConnection con, Object obj, String er String state2 = generateStateCode(con, srvError.getErrorNumber(), srvError.getErrorState()); SQLServerException chainException = new SQLServerException(obj, - SQLServerException.checkAndAppendClientConnId(srvError.getErrorMessage(), con), - state2, srvError, bStack); + SQLServerException.checkAndAppendClientConnId(srvError.getErrorMessage(), con), state2, + srvError, bStack); chainException.setDriverErrorCode(DRIVER_ERROR_FROM_DATABASE); - + theException.setNextException(chainException); } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java index cbfe102ca..12432c560 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java @@ -1707,32 +1707,32 @@ boolean onInfo(TDSReader tdsReader) throws SQLServerException { if (16954 == infoMessage.msg.getErrorNumber()) executedSqlDirectly = true; - // 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(); + 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; + 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 ); + 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; + infoMessage = (SQLServerInfoMessage) srvMessage; } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerWarning.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerWarning.java index 81d588bd9..de6df2a42 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerWarning.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerWarning.java @@ -6,31 +6,29 @@ import java.sql.SQLWarning; + /** * Holds information about SQL Server messages that is considered as Informational Messages (normally if SQL Server Severity is at 10) *

- * Instead of just holding the SQL Server message (like a normal SQLWarning, it also holds all the + * 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 *

* This enables client to print out extra information about the message.
* Like: In what procedure was the message produced. */ -public class SQLServerWarning -extends SQLWarning -{ +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); + super(sqlServerError.getErrorMessage(), SQLServerException.generateStateCode(null, + sqlServerError.getErrorNumber(), sqlServerError.getErrorState()), sqlServerError.getErrorNumber(), + null); this.sqlServerError = sqlServerError; } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java index eb96becbc..5c451b22b 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java @@ -25,7 +25,6 @@ import javax.transaction.xa.Xid; - /** * Implements Transaction id used to recover transactions. */ diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java index aa4252f95..b3e854fd1 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java @@ -66,8 +66,7 @@ protected Object[][] getContents() { {"R_invalidClientSecret", "AADSTS7000215: Invalid client secret provided"}, {"R_invalidCertFields", "Error reading certificate, please verify the location of the certificate.signed fields invalid"}, - {"R_invalidAADAuth", - "Failed to authenticate the user {0} in Active Directory (Authentication={1})"}, + {"R_invalidAADAuth", "Failed to authenticate the user {0} in Active Directory (Authentication={1})"}, {"R_failedValidate", "failed to validate values in $0} "}, {"R_tableNotDropped", "table not dropped. "}, {"R_connectionReset", "Connection reset"}, {"R_unknownException", "Unknown exception"}, {"R_deadConnection", "Dead connection should be invalid"}, diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java index 7a9ea31d4..4283a80f5 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java @@ -419,14 +419,16 @@ public static void dropTableIfExists(String tableName, java.sql.Statement stmt) dropObjectIfExists(tableName, "U", stmt); } - public static void dropTableWithSchemaIfExists(String tableNameWithSchema, java.sql.Statement stmt) throws SQLException { - stmt.execute("IF OBJECT_ID('" + tableNameWithSchema + "', 'U') IS NOT NULL DROP TABLE " + tableNameWithSchema + ";"); + public static void dropTableWithSchemaIfExists(String tableNameWithSchema, + java.sql.Statement stmt) throws SQLException { + stmt.execute( + "IF OBJECT_ID('" + tableNameWithSchema + "', 'U') IS NOT NULL DROP TABLE " + tableNameWithSchema + ";"); } - - public static void dropProcedureWithSchemaIfExists(String procedureWithSchema, java.sql.Statement stmt) throws SQLException { - stmt.execute("IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'" - + procedureWithSchema + "') AND type in (N'P', N'PC')) DROP PROCEDURE " + procedureWithSchema + ";"); + public static void dropProcedureWithSchemaIfExists(String procedureWithSchema, + java.sql.Statement stmt) throws SQLException { + stmt.execute("IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'" + procedureWithSchema + + "') AND type in (N'P', N'PC')) DROP PROCEDURE " + procedureWithSchema + ";"); } /** 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 97b0e3f92..ac7fb02f5 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyCSVTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyCSVTest.java @@ -166,7 +166,8 @@ public void testEscapeColumnDelimitersCSV() throws Exception { 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\"", ""}; + 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/callablestatement/CallableStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java index 512039392..5a502dab3 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java @@ -1160,10 +1160,12 @@ public void testFourPartSyntaxCallEscapeSyntax() throws SQLException { try (Statement stmt = connection.createStatement()) { stmt.execute("IF OBJECT_ID(N'" + table + "') IS NOT NULL DROP TABLE " + table); - stmt.execute("CREATE TABLE " + table + " (serverName varchar(100),network varchar(100),serverStatus varchar(4000), id int, collation varchar(100), connectTimeout int, queryTimeout int)"); + stmt.execute("CREATE TABLE " + table + + " (serverName varchar(100),network varchar(100),serverStatus varchar(4000), id int, collation varchar(100), connectTimeout int, queryTimeout int)"); stmt.execute("INSERT " + table + " EXEC sp_helpserver"); - ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM " + table + " WHERE serverName = N'" + linkedServer + "'"); + ResultSet rs = stmt + .executeQuery("SELECT COUNT(*) FROM " + table + " WHERE serverName = N'" + linkedServer + "'"); rs.next(); if (rs.getInt(1) == 1) { @@ -1171,7 +1173,8 @@ public void testFourPartSyntaxCallEscapeSyntax() throws SQLException { } stmt.execute("EXEC sp_addlinkedserver @server='" + linkedServer + "';"); - stmt.execute("EXEC sp_addlinkedsrvlogin @rmtsrvname=N'" + linkedServer + "', @rmtuser=N'" + remoteUser + "', @rmtpassword=N'" + remotePassword + "'"); + stmt.execute("EXEC sp_addlinkedsrvlogin @rmtsrvname=N'" + linkedServer + "', @rmtuser=N'" + remoteUser + + "', @rmtpassword=N'" + remotePassword + "'"); stmt.execute("EXEC sp_serveroption '" + linkedServer + "', 'rpc', true;"); stmt.execute("EXEC sp_serveroption '" + linkedServer + "', 'rpc out', true;"); } @@ -1183,11 +1186,14 @@ public void testFourPartSyntaxCallEscapeSyntax() throws SQLException { ds.setEncrypt(false); ds.setTrustServerCertificate(true); - try (Connection linkedServerConnection = ds.getConnection(); Statement stmt = linkedServerConnection.createStatement()) { - stmt.execute("create or alter procedure dbo.TestAdd(@Num1 int, @Num2 int, @Result int output) as begin set @Result = @Num1 + @Num2; end;"); + try (Connection linkedServerConnection = ds.getConnection(); + Statement stmt = linkedServerConnection.createStatement()) { + stmt.execute( + "create or alter procedure dbo.TestAdd(@Num1 int, @Num2 int, @Result int output) as begin set @Result = @Num1 + @Num2; end;"); } - try (CallableStatement cstmt = connection.prepareCall("{call [" + linkedServer + "].master.dbo.TestAdd(?,?,?)}")) { + try (CallableStatement cstmt = connection + .prepareCall("{call [" + linkedServer + "].master.dbo.TestAdd(?,?,?)}")) { int sum = 11; int param0 = 1; int param1 = 10; diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java index fee7e274f..044cc6a3e 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java @@ -1002,15 +1002,14 @@ public void shouldEscapeSchemaName() throws SQLException { try (Statement stmt = connection.createStatement()) { stmt.execute("CREATE SCHEMA " + schema); stmt.execute("CREATE TABLE " + tableNameWithSchema + " (id UNIQUEIDENTIFIER, name NVARCHAR(400));"); - stmt.execute("CREATE PROCEDURE " + sprocWithSchema + "(@id UNIQUEIDENTIFIER, @name VARCHAR(400)) AS " + - "BEGIN SET TRANSACTION ISOLATION LEVEL SERIALIZABLE BEGIN TRANSACTION UPDATE " + stmt.execute("CREATE PROCEDURE " + sprocWithSchema + "(@id UNIQUEIDENTIFIER, @name VARCHAR(400)) AS " + + "BEGIN SET TRANSACTION ISOLATION LEVEL SERIALIZABLE BEGIN TRANSACTION UPDATE " + tableNameWithSchema + " SET name = @name WHERE id = @id COMMIT END"); } try (Connection con = getConnection()) { DatabaseMetaData md = con.getMetaData(); - try (ResultSet procedures = md.getProcedures( - null, escapedSchema, "updateresource")) { + try (ResultSet procedures = md.getProcedures(null, escapedSchema, "updateresource")) { if (!procedures.next()) { fail("Escaped schema pattern did not succeed. No results found."); } 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 2e0968ebe..6eb7fc51d 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/GuidTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/GuidTest.java @@ -24,6 +24,8 @@ 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 */ @@ -49,8 +51,7 @@ public void testGuid() throws Exception { // Create the test table TestUtils.dropTableIfExists(escapedTableName, stmt); - String query = "create table " + escapedTableName - + " (uuid uniqueidentifier, id int IDENTITY primary key)"; + String query = "create table " + escapedTableName + " (uuid uniqueidentifier, id int IDENTITY primary key)"; stmt.executeUpdate(query); UUID uuid = UUID.randomUUID(); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java index 7fa028c2e..d4ba85fa5 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java @@ -465,7 +465,6 @@ public void testPreparedStatementShouldNotUseWrongHandleAfterReconnect() throws } } - @Test public void testUnprocessedResponseCountSuccessfulIdleConnectionRecovery() throws SQLException { try (SQLServerConnection con = (SQLServerConnection) ResiliencyUtils.getConnection(connectionString)) { From 78bdefd7264f4caba230fb11aacfd4643eb9a483 Mon Sep 17 00:00:00 2001 From: lilgreenbird Date: Wed, 3 Apr 2024 14:18:06 -0700 Subject: [PATCH 40/47] Updates for 12.7.0 (#2375) --- CHANGELOG.md | 27 +++++++++++++++++++++++++-- mssql-jdbc_auth_LICENSE | 2 +- pom.xml | 2 +- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56107db32..af8d7d65d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,29 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) +## [12.7.0] Preview Release +### Added +- Server Message Handler and SQLException Chaining [#2251] (https://github.com/microsoft/mssql-jdbc/pull/2251) +- Finish support for RFC4180 for CSV bulk insert operations [#2338] (https://github.com/microsoft/mssql-jdbc/pull/2338) +- Allow constructing a microsoft.sql.DateTimeOffset instance from a java.time.OffsetDateTime value [#2340] (https://github.com/microsoft/mssql-jdbc/pull/2340) +- Added support for TDSType.GUID [#2370] (https://github.com/microsoft/mssql-jdbc/pull/2370) + +### Changed +- Remove synchronized from Socket overrides [#2337] (https://github.com/microsoft/mssql-jdbc/pull/2337) +- Default to RMFAIL instead of RMERR [#2348] (https://github.com/microsoft/mssql-jdbc/pull/2348) + +### Fixed issues +- Fix to allow connection retries to be disabled by setting connectRetryCount to 0 [#2293] (https://github.com/microsoft/mssql-jdbc/pull/2293) +- Fix to ensure metadata returned follows JDBC data type specs [#2326] (https://github.com/microsoft/mssql-jdbc/pull/2326) +- Added token cache map to fix use of unintended auth token for subsequent connections [#2341] (https://github.com/microsoft/mssql-jdbc/pull/2341) +- Fix calling procedures with output parameters by their four-part syntax [#2349] (https://github.com/microsoft/mssql-jdbc/pull/2349) +- Clear prepared statement handle before reconnect [#2364] (https://github.com/microsoft/mssql-jdbc/pull/2364) +- Reset socketTimeout to original value after a successful connection open [#2355] (https://github.com/microsoft/mssql-jdbc/pull/2355) +- Clear prepared statement cache when resetting statement pool connection [#2361] (https://github.com/microsoft/mssql-jdbc/pull/2361) +- Fixed ClassLoader leak of ActivityCorrelator ThreadLocal [#2366] (https://github.com/microsoft/mssql-jdbc/pull/2366) +- Check if TDSCommand counter is null before incrementing. [#2368] (https://github.com/microsoft/mssql-jdbc/pull/2368) +- Escape schema for getProcedures and getProcedureColumns in SQLServerDatabaseMetaData [#2369] (https://github.com/microsoft/mssql-jdbc/pull/2369) + ## [12.6.0] Stable Release ### Changed - Adjusted PreparedStatement cache, so it's cleared before every execute [#2272](https://github.com/microsoft/mssql-jdbc/pull/2272) @@ -12,8 +35,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) ### Fixed issues - Fixed the way ActivityID was defined and used to be more in line with the behavior of other Microsoft drivers [#2254](https://github.com/microsoft/mssql-jdbc/pull/2254) -- - Fixed missing getters and setters for `useBulkCopyForBatchInsert` [#2277](https://github.com/microsoft/mssql-jdbc/pull/2277) -- - Fixed an issue where, when using the TOP qualifier in a query, the driver returns an error concerning ParameterMetadata [#2287](https://github.com/microsoft/mssql-jdbc/pull/2287) +- Fixed missing getters and setters for `useBulkCopyForBatchInsert` [#2277](https://github.com/microsoft/mssql-jdbc/pull/2277) +- Fixed an issue where, when using the TOP qualifier in a query, the driver returns an error concerning ParameterMetadata [#2287](https://github.com/microsoft/mssql-jdbc/pull/2287) - Fixed an issue where insert statements with missing whitespace worked correctly in regular cases, but not when using batch inserts [#2290](https://github.com/microsoft/mssql-jdbc/pull/2290) - Fixed timezone not being properly applied to Timestamps when inserted using batch insert with bulkcopy [#2291](https://github.com/microsoft/mssql-jdbc/pull/2291) - Fixed locks in IOBuffer to prevent deadlock issues that could arise [#2295](https://github.com/microsoft/mssql-jdbc/pull/2295) diff --git a/mssql-jdbc_auth_LICENSE b/mssql-jdbc_auth_LICENSE index eddb50467..08725e032 100644 --- a/mssql-jdbc_auth_LICENSE +++ b/mssql-jdbc_auth_LICENSE @@ -1,5 +1,5 @@ MICROSOFT SOFTWARE LICENSE TERMS -MICROSOFT JDBC DRIVER 12.6.0 FOR SQL SERVER +MICROSOFT JDBC DRIVER 12.7.0 FOR SQL SERVER These license terms are an agreement between you and Microsoft Corporation (or one of its affiliates). They apply to the software named above and any Microsoft services or software updates (except to the extent such services or updates are accompanied by new or additional terms, in which case those different terms apply prospectively and do not alter your or Microsoft’s rights relating to pre-updated software or services). IF YOU COMPLY WITH THESE LICENSE TERMS, YOU HAVE THE RIGHTS BELOW. BY USING THE SOFTWARE, YOU ACCEPT THESE TERMS. diff --git a/pom.xml b/pom.xml index 642cc6490..5c4ac0dc1 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.microsoft.sqlserver mssql-jdbc - 12.7.0-SNAPSHOT + 12.7.0 jar Microsoft JDBC Driver for SQL Server From 5b3f32ea0303225834c18b4d0fe24c0308788e2d Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Thu, 4 Apr 2024 11:37:45 -0700 Subject: [PATCH 41/47] Check that money/small money are within correct range for bulkcopy (#2379) * Fixed 2309 * CHANGELOG fix * Review fixes * More review + format * More changes --- CHANGELOG.md | 1 + .../com/microsoft/sqlserver/jdbc/DDC.java | 4 +- .../jdbc/bulkCopy/BulkCopyMoneyTest.java | 123 ++++++++++++++++++ .../sqlserver/testframework/Constants.java | 1 + 4 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyMoneyTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index af8d7d65d..77b38f4f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) - Fixed ClassLoader leak of ActivityCorrelator ThreadLocal [#2366] (https://github.com/microsoft/mssql-jdbc/pull/2366) - Check if TDSCommand counter is null before incrementing. [#2368] (https://github.com/microsoft/mssql-jdbc/pull/2368) - Escape schema for getProcedures and getProcedureColumns in SQLServerDatabaseMetaData [#2369] (https://github.com/microsoft/mssql-jdbc/pull/2369) +- Fix to properly validate money and small money values for BulkCopy [#2379] (https://github.com/microsoft/mssql-jdbc/pull/2379) ## [12.6.0] Stable Release ### Changed diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java b/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java index 57dab6643..03c95f65c 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java @@ -344,13 +344,14 @@ static final byte[] convertBigDecimalToBytes(BigDecimal bigDecimalVal, int scale return valueBytes; } - static final byte[] convertMoneyToBytes(BigDecimal bigDecimalVal, int bLength) { + static final byte[] convertMoneyToBytes(BigDecimal bigDecimalVal, int bLength) throws SQLServerException { byte[] valueBytes = new byte[bLength]; BigInteger bi = bigDecimalVal.unscaledValue(); if (bLength == 8) { // money + Util.validateMoneyRange(bigDecimalVal, JDBCType.MONEY); byte[] longbArray = new byte[bLength]; Util.writeLong(bi.longValue(), longbArray, 0); /* @@ -362,6 +363,7 @@ static final byte[] convertMoneyToBytes(BigDecimal bigDecimalVal, int bLength) { System.arraycopy(longbArray, 4, valueBytes, 0, 4); } else { // smallmoney + Util.validateMoneyRange(bigDecimalVal, JDBCType.SMALLMONEY); Util.writeInt(bi.intValue(), valueBytes, 0); } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyMoneyTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyMoneyTest.java new file mode 100644 index 000000000..bb954e95c --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyMoneyTest.java @@ -0,0 +1,123 @@ +package com.microsoft.sqlserver.jdbc.bulkCopy; + +import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.AfterAll; +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.RandomUtil; +import com.microsoft.sqlserver.jdbc.SQLServerBulkCSVFileRecord; +import com.microsoft.sqlserver.jdbc.SQLServerBulkCopy; +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 com.microsoft.sqlserver.testframework.Constants; +import com.microsoft.sqlserver.testframework.PrepUtil; + + +/** + * Tests money/smallmoney limits with BulkCopy + */ +@RunWith(JUnitPlatform.class) +public class BulkCopyMoneyTest extends AbstractTest { + static String encoding = Constants.UTF8; + static String delimiter = Constants.COMMA; + static String destTableName = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("moneyBulkCopyDest")); + static String destTableName2 = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("moneyBulkCopyDest")); + + @Test + /** + * Tests money and smallmoney with bulkcopy using minimum and maximum values of each + */ + public void testMoneyWithBulkCopy() throws Exception { + try (Connection conn = PrepUtil.getConnection(connectionString)) { + testMoneyLimits(-214799.3648, 922337203685387.5887, conn); // SMALLMONEY MIN + testMoneyLimits(214799.3698, 922337203685387.5887, conn); // SMALLMONEY MAX + testMoneyLimits(214719.3698, -922337203685497.5808, conn); // MONEY MIN + testMoneyLimits(214719.3698, 922337203685478.5807, conn); // MONEY MAX + } + } + + private void testMoneyLimits(double smallMoneyVal, double moneyVal, Connection conn) throws Exception { + SQLServerBulkCSVFileRecord fileRecord = constructFileRecord(smallMoneyVal, moneyVal); + + try { + testMoneyWithBulkCopy(conn, fileRecord); + fail(TestResource.getResource("R_expectedExceptionNotThrown")); + } catch (SQLException e) { + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_valueOutOfRange")), e.getMessage()); + } + } + + private SQLServerBulkCSVFileRecord constructFileRecord(double smallMoneyVal, double moneyVal) throws Exception { + Map data = new HashMap(); + data.put(smallMoneyVal, moneyVal); + + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("smallmoneycol, moneycol\n"); + + for (Map.Entry entry : data.entrySet()) { + stringBuilder.append(String.format("%s,%s\n", entry.getKey(), entry.getValue())); + } + + byte[] bytes = stringBuilder.toString().getBytes(StandardCharsets.UTF_8); + SQLServerBulkCSVFileRecord fileRecord = null; + try (InputStream inputStream = new ByteArrayInputStream(bytes)) { + fileRecord = new SQLServerBulkCSVFileRecord(inputStream, encoding, delimiter, true); + } + return fileRecord; + } + + private void testMoneyWithBulkCopy(Connection conn, SQLServerBulkCSVFileRecord fileRecord) throws SQLException { + try (SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(conn); Statement stmt = conn.createStatement()) { + + fileRecord.addColumnMetadata(1, "c1", java.sql.Types.DECIMAL, 10, 4); // with smallmoney + fileRecord.addColumnMetadata(2, "c2", java.sql.Types.DECIMAL, 19, 4); // with money + + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(fileRecord); + + try (ResultSet rs = stmt.executeQuery("select * FROM " + destTableName + " order by c1"); + SQLServerBulkCopy bcOperation = new SQLServerBulkCopy(conn);) { + bcOperation.setDestinationTableName(destTableName2); + bcOperation.writeToServer(rs); + } + } + } + + @BeforeAll + public static void setupTests() throws SQLException { + try (Connection con = getConnection(); Statement stmt = con.createStatement()) { + TestUtils.dropTableIfExists(destTableName, stmt); + TestUtils.dropTableIfExists(destTableName2, stmt); + + String table = "create table " + destTableName + " (c1 smallmoney, c2 money)"; + stmt.execute(table); + table = "create table " + destTableName2 + " (c1 smallmoney, c2 money)"; + stmt.execute(table); + } + } + + @AfterAll + public static void cleanUp() throws Exception { + try (Connection con = getConnection(); Statement stmt = con.createStatement()) { + TestUtils.dropTableIfExists(destTableName, stmt); + TestUtils.dropTableIfExists(destTableName2, stmt); + } + } +} diff --git a/src/test/java/com/microsoft/sqlserver/testframework/Constants.java b/src/test/java/com/microsoft/sqlserver/testframework/Constants.java index 92ba90811..0b1399c45 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/Constants.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/Constants.java @@ -179,6 +179,7 @@ private Constants() {} public static final String SEND_TEMPORAL_DATATYPES_AS_STRING_FOR_BULK_COPY = "SENDTEMPORALDATATYPESASSTRINGFORBULKCOPY"; public static final String PREPARE_METHOD = "PREPAREMETHOD"; public static final String CONFIG_PROPERTIES_FILE = "config.properties"; + public static final String UTF8 = "UTF-8"; public enum LOB { CLOB, From 67ba1f16a70f30c976b8bc3c6fb21e4b8867bb64 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Fri, 5 Apr 2024 11:05:53 -0700 Subject: [PATCH 42/47] 12.7.1 (#2385) --- CHANGELOG.md | 2 +- build.gradle | 2 +- mssql-jdbc_auth_LICENSE | 2 +- pom.xml | 2 +- src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77b38f4f8..1a8803af6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) -## [12.7.0] Preview Release +## [12.7.1] Preview Release ### Added - Server Message Handler and SQLException Chaining [#2251] (https://github.com/microsoft/mssql-jdbc/pull/2251) - Finish support for RFC4180 for CSV bulk insert operations [#2338] (https://github.com/microsoft/mssql-jdbc/pull/2338) diff --git a/build.gradle b/build.gradle index 97b53a4d4..33632b930 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ apply plugin: 'java' -version = '12.7.0' +version = '12.7.1' def releaseExt = '-preview' def jreVersion = "" def testOutputDir = file("build/classes/java/test") diff --git a/mssql-jdbc_auth_LICENSE b/mssql-jdbc_auth_LICENSE index 08725e032..7b1555e09 100644 --- a/mssql-jdbc_auth_LICENSE +++ b/mssql-jdbc_auth_LICENSE @@ -1,5 +1,5 @@ MICROSOFT SOFTWARE LICENSE TERMS -MICROSOFT JDBC DRIVER 12.7.0 FOR SQL SERVER +MICROSOFT JDBC DRIVER 12.7.1 FOR SQL SERVER These license terms are an agreement between you and Microsoft Corporation (or one of its affiliates). They apply to the software named above and any Microsoft services or software updates (except to the extent such services or updates are accompanied by new or additional terms, in which case those different terms apply prospectively and do not alter your or Microsoft’s rights relating to pre-updated software or services). IF YOU COMPLY WITH THESE LICENSE TERMS, YOU HAVE THE RIGHTS BELOW. BY USING THE SOFTWARE, YOU ACCEPT THESE TERMS. diff --git a/pom.xml b/pom.xml index 5c4ac0dc1..1e33a116a 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.microsoft.sqlserver mssql-jdbc - 12.7.0 + 12.7.1 jar Microsoft JDBC Driver for SQL Server diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java index 8cb90d58d..8a975e539 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java @@ -8,7 +8,7 @@ final class SQLJdbcVersion { static final int MAJOR = 12; static final int MINOR = 7; - static final int PATCH = 0; + static final int PATCH = 1; static final int BUILD = 0; /* * Used to load mssql-jdbc_auth DLL. From c073e78cd18153b305c29533fe53b02200edf796 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Fri, 5 Apr 2024 11:34:22 -0700 Subject: [PATCH 43/47] snapshot (#2386) --- build.gradle | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 33632b930..f862b13d4 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ apply plugin: 'java' -version = '12.7.1' +version = '12.7.1-SNAPSHOT' def releaseExt = '-preview' def jreVersion = "" def testOutputDir = file("build/classes/java/test") diff --git a/pom.xml b/pom.xml index 1e33a116a..7a123f3cf 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.microsoft.sqlserver mssql-jdbc - 12.7.1 + 12.7.1-SNAPSHOT jar Microsoft JDBC Driver for SQL Server From 67f7fab44d0e399932920751952acbf080d78790 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Wed, 10 Apr 2024 10:27:34 -0700 Subject: [PATCH 44/47] Changes to MONEY/SMALLMONEY PR (#2379) (#2383) * Changes as per prior code review * setupMoneyTests can be added to beforeAll, it should be ran each time * Passed in wrong values for tests --- .../jdbc/bulkCopy/BulkCopyAllTypesTest.java | 128 ++++++++++++++++-- .../jdbc/bulkCopy/BulkCopyMoneyTest.java | 123 ----------------- .../sqlserver/testframework/Constants.java | 4 + 3 files changed, 117 insertions(+), 138 deletions(-) delete mode 100644 src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyMoneyTest.java 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 660406ce5..a78e53161 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyAllTypesTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyAllTypesTest.java @@ -4,13 +4,33 @@ */ package com.microsoft.sqlserver.jdbc.bulkCopy; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; 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 java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +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 org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -20,8 +40,10 @@ import com.microsoft.sqlserver.jdbc.ComparisonUtil; import com.microsoft.sqlserver.jdbc.RandomData; import com.microsoft.sqlserver.jdbc.RandomUtil; +import com.microsoft.sqlserver.jdbc.SQLServerBulkCSVFileRecord; import com.microsoft.sqlserver.jdbc.SQLServerBulkCopy; import com.microsoft.sqlserver.jdbc.SQLServerBulkCopyOptions; +import com.microsoft.sqlserver.jdbc.TestResource; import com.microsoft.sqlserver.jdbc.TestUtils; import com.microsoft.sqlserver.testframework.AbstractSQLGenerator; import com.microsoft.sqlserver.testframework.AbstractTest; @@ -31,20 +53,6 @@ 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 { @@ -55,12 +63,26 @@ public class BulkCopyAllTypesTest extends AbstractTest { @BeforeAll public static void setupTests() throws Exception { setConnection(); + setupMoneyTests(); + } + + public static void setupMoneyTests() throws SQLException { + try (Connection con = getConnection(); Statement stmt = con.createStatement()) { + TestUtils.dropTableIfExists(destTableName, stmt); + TestUtils.dropTableIfExists(destTableName2, stmt); + + String table = "create table " + destTableName + " (c1 smallmoney, c2 money)"; + stmt.execute(table); + table = "create table " + destTableName2 + " (c1 smallmoney, c2 money)"; + stmt.execute(table); + } } /** * Test TVP with result set - * + * * @throws SQLException + * an exception */ @Test @Tag(Constants.xAzureSQLDW) @@ -127,6 +149,12 @@ private void terminateVariation() throws SQLException { private static final String dateTimeTestTable = AbstractSQLGenerator .escapeIdentifier(RandomUtil.getIdentifier("bulkCopyTimestampTest")); + /** + * Test money/smallmoney with BulkCopy + * + * @throws SQLException + * an exception + */ @Test public void testBulkCopyTimestamp() throws SQLException { List timeStamps = new ArrayList<>(); @@ -185,4 +213,74 @@ public void testBulkCopyTimestamp() throws SQLException { private static long getTime(Timestamp time) { return (3 * time.getTime() + 5) / 10; } + + static String encoding = Constants.UTF8; + static String delimiter = Constants.COMMA; + static String destTableName = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("moneyBulkCopyDest")); + static String destTableName2 = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("moneyBulkCopyDest")); + + @Test + public void testMoneyWithBulkCopy() throws Exception { + try (Connection conn = PrepUtil.getConnection(connectionString)) { + testMoneyLimits(Constants.MIN_VALUE_SMALLMONEY - 1, Constants.MAX_VALUE_MONEY - 1, conn); // 1 less than SMALLMONEY MIN + testMoneyLimits(Constants.MAX_VALUE_SMALLMONEY + 1, Constants.MAX_VALUE_MONEY - 1, conn); // 1 more than SMALLMONEY MAX + testMoneyLimits(Constants.MAX_VALUE_SMALLMONEY - 1, Constants.MIN_VALUE_MONEY - 1, conn); // 1 less than MONEY MIN + testMoneyLimits(Constants.MAX_VALUE_SMALLMONEY - 1, Constants.MAX_VALUE_MONEY + 1, conn); // 1 more than MONEY MAX + } + } + + private void testMoneyLimits(double smallMoneyVal, double moneyVal, Connection conn) throws Exception { + SQLServerBulkCSVFileRecord fileRecord = constructFileRecord(smallMoneyVal, moneyVal); + + try { + testMoneyWithBulkCopy(conn, fileRecord); + fail(TestResource.getResource("R_expectedExceptionNotThrown")); + } catch (SQLException e) { + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_valueOutOfRange")), e.getMessage()); + } + } + + private SQLServerBulkCSVFileRecord constructFileRecord(double smallMoneyVal, double moneyVal) throws Exception { + Map data = new HashMap(); + data.put(smallMoneyVal, moneyVal); + + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("smallmoneycol, moneycol\n"); + + for (Map.Entry entry : data.entrySet()) { + stringBuilder.append(String.format("%s,%s\n", entry.getKey(), entry.getValue())); + } + + byte[] bytes = stringBuilder.toString().getBytes(StandardCharsets.UTF_8); + SQLServerBulkCSVFileRecord fileRecord; + try (InputStream inputStream = new ByteArrayInputStream(bytes)) { + fileRecord = new SQLServerBulkCSVFileRecord(inputStream, encoding, delimiter, true); + } + return fileRecord; + } + + private void testMoneyWithBulkCopy(Connection conn, SQLServerBulkCSVFileRecord fileRecord) throws SQLException { + try (SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(conn); Statement stmt = conn.createStatement()) { + + fileRecord.addColumnMetadata(1, "c1", java.sql.Types.DECIMAL, 10, 4); // with smallmoney + fileRecord.addColumnMetadata(2, "c2", java.sql.Types.DECIMAL, 19, 4); // with money + + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(fileRecord); + + try (ResultSet rs = stmt.executeQuery("select * FROM " + destTableName + " order by c1"); + SQLServerBulkCopy bcOperation = new SQLServerBulkCopy(conn)) { + bcOperation.setDestinationTableName(destTableName2); + bcOperation.writeToServer(rs); + } + } + } + + @AfterAll + public static void cleanUp() throws Exception { + try (Connection con = getConnection(); Statement stmt = con.createStatement()) { + TestUtils.dropTableIfExists(destTableName, stmt); + TestUtils.dropTableIfExists(destTableName2, stmt); + } + } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyMoneyTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyMoneyTest.java deleted file mode 100644 index bb954e95c..000000000 --- a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyMoneyTest.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.microsoft.sqlserver.jdbc.bulkCopy; - -import static org.junit.Assert.fail; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.HashMap; -import java.util.Map; - -import org.junit.jupiter.api.AfterAll; -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.RandomUtil; -import com.microsoft.sqlserver.jdbc.SQLServerBulkCSVFileRecord; -import com.microsoft.sqlserver.jdbc.SQLServerBulkCopy; -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 com.microsoft.sqlserver.testframework.Constants; -import com.microsoft.sqlserver.testframework.PrepUtil; - - -/** - * Tests money/smallmoney limits with BulkCopy - */ -@RunWith(JUnitPlatform.class) -public class BulkCopyMoneyTest extends AbstractTest { - static String encoding = Constants.UTF8; - static String delimiter = Constants.COMMA; - static String destTableName = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("moneyBulkCopyDest")); - static String destTableName2 = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("moneyBulkCopyDest")); - - @Test - /** - * Tests money and smallmoney with bulkcopy using minimum and maximum values of each - */ - public void testMoneyWithBulkCopy() throws Exception { - try (Connection conn = PrepUtil.getConnection(connectionString)) { - testMoneyLimits(-214799.3648, 922337203685387.5887, conn); // SMALLMONEY MIN - testMoneyLimits(214799.3698, 922337203685387.5887, conn); // SMALLMONEY MAX - testMoneyLimits(214719.3698, -922337203685497.5808, conn); // MONEY MIN - testMoneyLimits(214719.3698, 922337203685478.5807, conn); // MONEY MAX - } - } - - private void testMoneyLimits(double smallMoneyVal, double moneyVal, Connection conn) throws Exception { - SQLServerBulkCSVFileRecord fileRecord = constructFileRecord(smallMoneyVal, moneyVal); - - try { - testMoneyWithBulkCopy(conn, fileRecord); - fail(TestResource.getResource("R_expectedExceptionNotThrown")); - } catch (SQLException e) { - assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_valueOutOfRange")), e.getMessage()); - } - } - - private SQLServerBulkCSVFileRecord constructFileRecord(double smallMoneyVal, double moneyVal) throws Exception { - Map data = new HashMap(); - data.put(smallMoneyVal, moneyVal); - - StringBuilder stringBuilder = new StringBuilder(); - stringBuilder.append("smallmoneycol, moneycol\n"); - - for (Map.Entry entry : data.entrySet()) { - stringBuilder.append(String.format("%s,%s\n", entry.getKey(), entry.getValue())); - } - - byte[] bytes = stringBuilder.toString().getBytes(StandardCharsets.UTF_8); - SQLServerBulkCSVFileRecord fileRecord = null; - try (InputStream inputStream = new ByteArrayInputStream(bytes)) { - fileRecord = new SQLServerBulkCSVFileRecord(inputStream, encoding, delimiter, true); - } - return fileRecord; - } - - private void testMoneyWithBulkCopy(Connection conn, SQLServerBulkCSVFileRecord fileRecord) throws SQLException { - try (SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(conn); Statement stmt = conn.createStatement()) { - - fileRecord.addColumnMetadata(1, "c1", java.sql.Types.DECIMAL, 10, 4); // with smallmoney - fileRecord.addColumnMetadata(2, "c2", java.sql.Types.DECIMAL, 19, 4); // with money - - bulkCopy.setDestinationTableName(destTableName); - bulkCopy.writeToServer(fileRecord); - - try (ResultSet rs = stmt.executeQuery("select * FROM " + destTableName + " order by c1"); - SQLServerBulkCopy bcOperation = new SQLServerBulkCopy(conn);) { - bcOperation.setDestinationTableName(destTableName2); - bcOperation.writeToServer(rs); - } - } - } - - @BeforeAll - public static void setupTests() throws SQLException { - try (Connection con = getConnection(); Statement stmt = con.createStatement()) { - TestUtils.dropTableIfExists(destTableName, stmt); - TestUtils.dropTableIfExists(destTableName2, stmt); - - String table = "create table " + destTableName + " (c1 smallmoney, c2 money)"; - stmt.execute(table); - table = "create table " + destTableName2 + " (c1 smallmoney, c2 money)"; - stmt.execute(table); - } - } - - @AfterAll - public static void cleanUp() throws Exception { - try (Connection con = getConnection(); Statement stmt = con.createStatement()) { - TestUtils.dropTableIfExists(destTableName, stmt); - TestUtils.dropTableIfExists(destTableName2, stmt); - } - } -} diff --git a/src/test/java/com/microsoft/sqlserver/testframework/Constants.java b/src/test/java/com/microsoft/sqlserver/testframework/Constants.java index 0b1399c45..e4338fc92 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/Constants.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/Constants.java @@ -180,6 +180,10 @@ private Constants() {} public static final String PREPARE_METHOD = "PREPAREMETHOD"; public static final String CONFIG_PROPERTIES_FILE = "config.properties"; public static final String UTF8 = "UTF-8"; + public static final double MAX_VALUE_MONEY = 922337203685477.5807; + public static final double MIN_VALUE_MONEY = -922337203685477.5808; + public static final double MAX_VALUE_SMALLMONEY = 214748.3647; + public static final double MIN_VALUE_SMALLMONEY = -214748.3648; public enum LOB { CLOB, From 193edbdaf085786fbe47227e80b88d942e0547ad Mon Sep 17 00:00:00 2001 From: Codegass Date: Wed, 10 Apr 2024 15:55:02 -0400 Subject: [PATCH 45/47] Bring JUnit Jupiter Assumptions to `RequestBoundaryMethodsTest` to Avoid Test Case Quiet Quit (#2382) * Bring JUnit Jupiter Assumptions to RequestBoundaryMethodsTest to Avoid Test Case Quiet Quit * Update src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java Co-authored-by: Jeff Wasty --------- Co-authored-by: Jeff Wasty --- .../RequestBoundaryMethodsTest.java | 400 +++++++++--------- 1 file changed, 197 insertions(+), 203 deletions(-) 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 dbdec02cf..f0c1d909f 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java @@ -8,6 +8,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -92,71 +93,70 @@ public void testModifiableConnectionProperties() throws SQLException { boolean ignoreOffsetOnDateTimeOffsetConversion2 = false; try (SQLServerConnection con = getConnection(); Statement stmt = con.createStatement()) { - if (TestUtils.isJDBC43OrGreater(con)) { - // Second database - stmt.executeUpdate("CREATE DATABASE [" + sCatalog2 + "]"); - - // First set of values. - setConnectionFields(con, autoCommitMode1, transactionIsolationLevel1, networkTimeout1, holdability1, - sendTimeAsDatetime1, statementPoolingCacheSize1, disableStatementPooling1, - serverPreparedStatementDiscardThreshold1, enablePrepareOnFirstPreparedStatementCall1, sCatalog1, - 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, - 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, - 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, - ignoreOffsetOnDateTimeOffsetConversion2); - con.beginRequest(); - setConnectionFields(con, autoCommitMode1, transactionIsolationLevel1, networkTimeout1, holdability1, - sendTimeAsDatetime1, statementPoolingCacheSize1, disableStatementPooling1, - serverPreparedStatementDiscardThreshold1, enablePrepareOnFirstPreparedStatementCall1, sCatalog1, - 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, - 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, - ignoreOffsetOnDateTimeOffsetConversion1); - setConnectionFields(con, autoCommitMode2, transactionIsolationLevel2, networkTimeout2, holdability2, - sendTimeAsDatetime2, statementPoolingCacheSize2, disableStatementPooling2, - serverPreparedStatementDiscardThreshold2, enablePrepareOnFirstPreparedStatementCall2, sCatalog2, - useBulkCopyForBatchInsert2, useFmtOnly2, delayLoadingLobs2, - ignoreOffsetOnDateTimeOffsetConversion2); - con.endRequest(); - // No change. - compareValuesAgainstConnection(con, autoCommitMode2, transactionIsolationLevel2, networkTimeout2, - holdability2, sendTimeAsDatetime2, statementPoolingCacheSize2, disableStatementPooling2, - serverPreparedStatementDiscardThreshold2, enablePrepareOnFirstPreparedStatementCall2, sCatalog2, - useBulkCopyForBatchInsert2, useFmtOnly2, delayLoadingLobs2, - ignoreOffsetOnDateTimeOffsetConversion2); - } + assumeTrue(TestUtils.isJDBC43OrGreater(con)); + // Second database + stmt.executeUpdate("CREATE DATABASE [" + sCatalog2 + "]"); + + // First set of values. + setConnectionFields(con, autoCommitMode1, transactionIsolationLevel1, networkTimeout1, holdability1, + sendTimeAsDatetime1, statementPoolingCacheSize1, disableStatementPooling1, + serverPreparedStatementDiscardThreshold1, enablePrepareOnFirstPreparedStatementCall1, sCatalog1, + 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, + 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, + 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, + ignoreOffsetOnDateTimeOffsetConversion2); + con.beginRequest(); + setConnectionFields(con, autoCommitMode1, transactionIsolationLevel1, networkTimeout1, holdability1, + sendTimeAsDatetime1, statementPoolingCacheSize1, disableStatementPooling1, + serverPreparedStatementDiscardThreshold1, enablePrepareOnFirstPreparedStatementCall1, sCatalog1, + 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, + 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, + ignoreOffsetOnDateTimeOffsetConversion1); + setConnectionFields(con, autoCommitMode2, transactionIsolationLevel2, networkTimeout2, holdability2, + sendTimeAsDatetime2, statementPoolingCacheSize2, disableStatementPooling2, + serverPreparedStatementDiscardThreshold2, enablePrepareOnFirstPreparedStatementCall2, sCatalog2, + useBulkCopyForBatchInsert2, useFmtOnly2, delayLoadingLobs2, + ignoreOffsetOnDateTimeOffsetConversion2); + con.endRequest(); + // No change. + compareValuesAgainstConnection(con, autoCommitMode2, transactionIsolationLevel2, networkTimeout2, + holdability2, sendTimeAsDatetime2, statementPoolingCacheSize2, disableStatementPooling2, + serverPreparedStatementDiscardThreshold2, enablePrepareOnFirstPreparedStatementCall2, sCatalog2, + useBulkCopyForBatchInsert2, useFmtOnly2, delayLoadingLobs2, + ignoreOffsetOnDateTimeOffsetConversion2); } finally { TestUtils.dropDatabaseIfExists(sCatalog2, connectionString); } @@ -170,24 +170,23 @@ public void testModifiableConnectionProperties() throws SQLException { @Test public void testWarnings() throws SQLException { try (Connection con = getConnection()) { - if (TestUtils.isJDBC43OrGreater(con)) { - con.beginRequest(); - generateWarning(con); - assertNotNull(con.getWarnings()); - con.endRequest(); - assertNull(con.getWarnings()); - - generateWarning(con); - con.endRequest(); - assertNotNull(con.getWarnings()); - - con.clearWarnings(); - con.beginRequest(); - generateWarning(con); - con.beginRequest(); - con.endRequest(); - assertNull(con.getWarnings()); - } + assumeTrue(TestUtils.isJDBC43OrGreater(con)); + con.beginRequest(); + generateWarning(con); + assertNotNull(con.getWarnings()); + con.endRequest(); + assertNull(con.getWarnings()); + + generateWarning(con); + con.endRequest(); + assertNotNull(con.getWarnings()); + + con.clearWarnings(); + con.beginRequest(); + generateWarning(con); + con.beginRequest(); + con.endRequest(); + assertNull(con.getWarnings()); } } @@ -199,23 +198,22 @@ public void testWarnings() throws SQLException { @Test public void testOpenTransactions() throws SQLException { try (Connection con = getConnection(); Statement stmt = con.createStatement()) { - if (TestUtils.isJDBC43OrGreater(con)) { - TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt); - stmt.executeUpdate("CREATE TABLE " + AbstractSQLGenerator.escapeIdentifier(tableName) + " (col int)"); - con.beginRequest(); - con.setAutoCommit(false); - stmt.executeUpdate("INSERT INTO " + AbstractSQLGenerator.escapeIdentifier(tableName) + " values(5)"); - // endRequest() does a rollback here, the value does not get inserted into the table. - con.endRequest(); - con.commit(); - - try (ResultSet rs = con.createStatement() - .executeQuery("SELECT * from " + AbstractSQLGenerator.escapeIdentifier(tableName))) { - assertTrue(!rs.isBeforeFirst(), "Should not have returned a result set."); - } finally { - if (null != tableName) { - TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt); - } + assumeTrue(TestUtils.isJDBC43OrGreater(con)); + TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt); + stmt.executeUpdate("CREATE TABLE " + AbstractSQLGenerator.escapeIdentifier(tableName) + " (col int)"); + con.beginRequest(); + con.setAutoCommit(false); + stmt.executeUpdate("INSERT INTO " + AbstractSQLGenerator.escapeIdentifier(tableName) + " values(5)"); + // endRequest() does a rollback here, the value does not get inserted into the table. + con.endRequest(); + con.commit(); + + try (ResultSet rs = con.createStatement() + .executeQuery("SELECT * from " + AbstractSQLGenerator.escapeIdentifier(tableName))) { + assertTrue(!rs.isBeforeFirst(), "Should not have returned a result set."); + } finally { + if (null != tableName) { + TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt); } } } catch (Exception e) { @@ -231,61 +229,58 @@ public void testOpenTransactions() throws SQLException { @Test public void testStatements() throws SQLException { try (Connection con = getConnection();) { - if (TestUtils.isJDBC43OrGreater(con)) { - try (Statement stmt1 = con.createStatement()) { - con.beginRequest(); - try (Statement stmt = con.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT 1")) { - rs.next(); - assertEquals(1, rs.getInt(1)); - con.endRequest(); - - assertTrue(!stmt1.isClosed(), - "Statement created outside of beginRequest()/endRequest() block should not be closed."); - assertTrue(stmt.isClosed(), - "Statement created inside beginRequest()/endRequest() block should be closed after endRequest()."); - assertTrue(rs.isClosed(), "ResultSet should be closed after endRequest()."); - } + assumeTrue(TestUtils.isJDBC43OrGreater(con)); + try (Statement stmt1 = con.createStatement()) { + con.beginRequest(); + try (Statement stmt = con.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT 1")) { + rs.next(); + assertEquals(1, rs.getInt(1)); + con.endRequest(); + + assertTrue(!stmt1.isClosed(), + "Statement created outside of beginRequest()/endRequest() block should not be closed."); + assertTrue(stmt.isClosed(), + "Statement created inside beginRequest()/endRequest() block should be closed after endRequest()."); + assertTrue(rs.isClosed(), "ResultSet should be closed after endRequest()."); } } + } - // Multiple statements inside beginRequest()/endRequest() block - con.beginRequest(); - try (Statement stmt = con.createStatement()) { - TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt); - stmt.executeUpdate( - "CREATE TABLE " + AbstractSQLGenerator.escapeIdentifier(tableName) + " (col int)"); - try (PreparedStatement ps = con.prepareStatement( - "INSERT INTO " + AbstractSQLGenerator.escapeIdentifier(tableName) + " values (?)")) { - ps.setInt(1, 2); - ps.executeUpdate(); - - try (Statement stmt1 = con.createStatement(); ResultSet rs = stmt1 - .executeQuery("SELECT * FROM " + AbstractSQLGenerator.escapeIdentifier(tableName))) { - rs.next(); - assertEquals(2, rs.getInt(1)); - TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt); - - try (CallableStatement cs = con.prepareCall("{call sp_server_info}")) { - cs.execute(); - con.endRequest(); - - assertTrue(stmt.isClosed()); - assertTrue(ps.isClosed()); - assertTrue(stmt1.isClosed()); - assertTrue(cs.isClosed()); - assertTrue(rs.isClosed()); - } + // Multiple statements inside beginRequest()/endRequest() block + con.beginRequest(); + try (Statement stmt = con.createStatement()) { + TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt); + stmt.executeUpdate("CREATE TABLE " + AbstractSQLGenerator.escapeIdentifier(tableName) + " (col int)"); + try (PreparedStatement ps = con.prepareStatement( + "INSERT INTO " + AbstractSQLGenerator.escapeIdentifier(tableName) + " values (?)")) { + ps.setInt(1, 2); + ps.executeUpdate(); + + try (Statement stmt1 = con.createStatement(); ResultSet rs = stmt1 + .executeQuery("SELECT * FROM " + AbstractSQLGenerator.escapeIdentifier(tableName))) { + rs.next(); + assertEquals(2, rs.getInt(1)); + TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt); + + try (CallableStatement cs = con.prepareCall("{call sp_server_info}")) { + cs.execute(); + con.endRequest(); + + assertTrue(stmt.isClosed()); + assertTrue(ps.isClosed()); + assertTrue(stmt1.isClosed()); + assertTrue(cs.isClosed()); + assertTrue(rs.isClosed()); } } - } finally { - if (null != tableName) { - try (Statement stmt = con.createStatement()) { - TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt); - } + } + } finally { + if (null != tableName) { + try (Statement stmt = con.createStatement()) { + TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt); } } - } } } @@ -307,67 +302,66 @@ class Variables { final CountDownLatch latch = new CountDownLatch(3); try { sharedVariables.con = getConnection(); - if (TestUtils.isJDBC43OrGreater(sharedVariables.con)) { - Thread thread1 = new Thread() { - public void run() { - try { - sharedVariables.con.setNetworkTimeout(null, 100); - sharedVariables.con.setHoldability(ResultSet.CLOSE_CURSORS_AT_COMMIT); + assumeTrue(TestUtils.isJDBC43OrGreater(sharedVariables.con)); + Thread thread1 = new Thread() { + public void run() { + try { + sharedVariables.con.setNetworkTimeout(null, 100); + sharedVariables.con.setHoldability(ResultSet.CLOSE_CURSORS_AT_COMMIT); + latch.countDown(); + } catch (SQLException e) { + e.printStackTrace(); + Thread.currentThread().interrupt(); + } + } + }; + + Thread thread2 = new Thread() { + public void run() { + try { + sharedVariables.stmt = sharedVariables.con.createStatement(); + try (ResultSet rs = sharedVariables.stmt.executeQuery("SELECT 1")) { + rs.next(); + assertEquals(1, rs.getInt(1)); latch.countDown(); - } catch (SQLException e) { - e.printStackTrace(); - Thread.currentThread().interrupt(); } + } catch (SQLException e) { + e.printStackTrace(); + Thread.currentThread().interrupt(); } - }; - - Thread thread2 = new Thread() { - public void run() { - try { - sharedVariables.stmt = sharedVariables.con.createStatement(); - try (ResultSet rs = sharedVariables.stmt.executeQuery("SELECT 1")) { - rs.next(); - assertEquals(1, rs.getInt(1)); - latch.countDown(); - } - } catch (SQLException e) { - e.printStackTrace(); - Thread.currentThread().interrupt(); + } + }; + + Thread thread3 = new Thread() { + public void run() { + try { + sharedVariables.pstmt = sharedVariables.con.prepareStatement("SELECT 1"); + try (ResultSet rs = sharedVariables.pstmt.executeQuery()) { + rs.next(); + assertEquals(1, rs.getInt(1)); + latch.countDown(); } + } catch (SQLException e) { + e.printStackTrace(); + Thread.currentThread().interrupt(); } - }; - - Thread thread3 = new Thread() { - public void run() { - try { - sharedVariables.pstmt = sharedVariables.con.prepareStatement("SELECT 1"); - try (ResultSet rs = sharedVariables.pstmt.executeQuery()) { - rs.next(); - assertEquals(1, rs.getInt(1)); - latch.countDown(); - } - } catch (SQLException e) { - e.printStackTrace(); - Thread.currentThread().interrupt(); - } - } - }; - - int originalNetworkTimeout = sharedVariables.con.getNetworkTimeout(); - int originalHoldability = sharedVariables.con.getHoldability(); - sharedVariables.con.beginRequest(); - thread1.start(); - thread2.start(); - thread3.start(); - latch.await(); - sharedVariables.con.endRequest(); - - assertEquals(originalNetworkTimeout, sharedVariables.con.getNetworkTimeout()); - assertEquals(originalHoldability, sharedVariables.con.getHoldability()); - assertTrue(sharedVariables.stmt.isClosed()); - assertTrue(sharedVariables.pstmt.isClosed()); - } + } + }; + + int originalNetworkTimeout = sharedVariables.con.getNetworkTimeout(); + int originalHoldability = sharedVariables.con.getHoldability(); + sharedVariables.con.beginRequest(); + thread1.start(); + thread2.start(); + thread3.start(); + latch.await(); + sharedVariables.con.endRequest(); + + assertEquals(originalNetworkTimeout, sharedVariables.con.getNetworkTimeout()); + assertEquals(originalHoldability, sharedVariables.con.getHoldability()); + assertTrue(sharedVariables.stmt.isClosed()); + assertTrue(sharedVariables.pstmt.isClosed()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); fail(e.getMessage()); From 276b4e5131fb877988ae91400b3a64f01d1d05b9 Mon Sep 17 00:00:00 2001 From: lilgreenbird Date: Thu, 11 Apr 2024 16:31:51 -0700 Subject: [PATCH 46/47] fix for gradle (#2389) --- .../sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java | 2 -- 1 file changed, 2 deletions(-) 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 f0c1d909f..9c1a7b2d9 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java @@ -216,8 +216,6 @@ public void testOpenTransactions() throws SQLException { TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt); } } - } catch (Exception e) { - fail(e.getMessage()); } } From ae6d4efaaa7204a722a1c4daedd6417c223a0d3f Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Fri, 12 Apr 2024 11:44:09 -0700 Subject: [PATCH 47/47] Fixed 12.7 changelog + small changelog sp. fixes. (#2390) * Update CHANGELOG.md * Update CHANGELOG.md --- CHANGELOG.md | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a8803af6..945c12e34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,29 +3,29 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) -## [12.7.1] Preview Release +## [12.7.0] Preview Release ### Added -- Server Message Handler and SQLException Chaining [#2251] (https://github.com/microsoft/mssql-jdbc/pull/2251) -- Finish support for RFC4180 for CSV bulk insert operations [#2338] (https://github.com/microsoft/mssql-jdbc/pull/2338) -- Allow constructing a microsoft.sql.DateTimeOffset instance from a java.time.OffsetDateTime value [#2340] (https://github.com/microsoft/mssql-jdbc/pull/2340) -- Added support for TDSType.GUID [#2370] (https://github.com/microsoft/mssql-jdbc/pull/2370) +- Server Message Handler and SQLException Chaining [#2251](https://github.com/microsoft/mssql-jdbc/pull/2251) +- Finish support for RFC4180 for CSV bulk insert operations [#2338](https://github.com/microsoft/mssql-jdbc/pull/2338) +- Allow constructing a microsoft.sql.DateTimeOffset instance from a java.time.OffsetDateTime value [#2340](https://github.com/microsoft/mssql-jdbc/pull/2340) +- Added support for TDSType.GUID [#2370](https://github.com/microsoft/mssql-jdbc/pull/2370) ### Changed -- Remove synchronized from Socket overrides [#2337] (https://github.com/microsoft/mssql-jdbc/pull/2337) -- Default to RMFAIL instead of RMERR [#2348] (https://github.com/microsoft/mssql-jdbc/pull/2348) +- Remove synchronized from Socket overrides [#2337](https://github.com/microsoft/mssql-jdbc/pull/2337) +- Default to RMFAIL instead of RMERR [#2348](https://github.com/microsoft/mssql-jdbc/pull/2348) ### Fixed issues -- Fix to allow connection retries to be disabled by setting connectRetryCount to 0 [#2293] (https://github.com/microsoft/mssql-jdbc/pull/2293) -- Fix to ensure metadata returned follows JDBC data type specs [#2326] (https://github.com/microsoft/mssql-jdbc/pull/2326) -- Added token cache map to fix use of unintended auth token for subsequent connections [#2341] (https://github.com/microsoft/mssql-jdbc/pull/2341) -- Fix calling procedures with output parameters by their four-part syntax [#2349] (https://github.com/microsoft/mssql-jdbc/pull/2349) -- Clear prepared statement handle before reconnect [#2364] (https://github.com/microsoft/mssql-jdbc/pull/2364) -- Reset socketTimeout to original value after a successful connection open [#2355] (https://github.com/microsoft/mssql-jdbc/pull/2355) -- Clear prepared statement cache when resetting statement pool connection [#2361] (https://github.com/microsoft/mssql-jdbc/pull/2361) -- Fixed ClassLoader leak of ActivityCorrelator ThreadLocal [#2366] (https://github.com/microsoft/mssql-jdbc/pull/2366) -- Check if TDSCommand counter is null before incrementing. [#2368] (https://github.com/microsoft/mssql-jdbc/pull/2368) -- Escape schema for getProcedures and getProcedureColumns in SQLServerDatabaseMetaData [#2369] (https://github.com/microsoft/mssql-jdbc/pull/2369) -- Fix to properly validate money and small money values for BulkCopy [#2379] (https://github.com/microsoft/mssql-jdbc/pull/2379) +- Fix to allow connection retries to be disabled by setting connectRetryCount to 0 [#2293](https://github.com/microsoft/mssql-jdbc/pull/2293) +- Fix to ensure metadata returned follows JDBC data type specs [#2326](https://github.com/microsoft/mssql-jdbc/pull/2326) +- Added token cache map to fix use of unintended auth token for subsequent connections [#2341](https://github.com/microsoft/mssql-jdbc/pull/2341) +- Fix calling procedures with output parameters by their four-part syntax [#2349](https://github.com/microsoft/mssql-jdbc/pull/2349) +- Reset socketTimeout to original value after a successful connection open [#2355](https://github.com/microsoft/mssql-jdbc/pull/2355) +- Clear prepared statement cache when resetting statement pool connection [#2361](https://github.com/microsoft/mssql-jdbc/pull/2361) +- Clear prepared statement handle before reconnect [#2364](https://github.com/microsoft/mssql-jdbc/pull/2364) +- Fixed ClassLoader leak of ActivityCorrelator ThreadLocal [#2366](https://github.com/microsoft/mssql-jdbc/pull/2366) +- Check if TDSCommand counter is null before incrementing [#2368](https://github.com/microsoft/mssql-jdbc/pull/2368) +- Escape schema for getProcedures and getProcedureColumns in SQLServerDatabaseMetaData [#2369](https://github.com/microsoft/mssql-jdbc/pull/2369) +- Fix to properly validate money and small money values for BulkCopy [#2379](https://github.com/microsoft/mssql-jdbc/pull/2379) ## [12.6.0] Stable Release ### Changed @@ -129,7 +129,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) - Fixed BigDecimal Precision/Scale issue [2051](https://github.com/microsoft/mssql-jdbc/pull/2051) - Fixed NULL state and 0 error code for SQL exceptions [2018](https://github.com/microsoft/mssql-jdbc/pull/2018) - Fixed incorrect updateCount [2013](https://github.com/microsoft/mssql-jdbc/pull/2013) -- Fixed Azure Active Directory user name cache matching to be case insensitive [1923](https://github.com/microsoft/mssql-jdbc/pull/1923) +- Fixed Azure Active Directory username cache matching to be case insensitive [1923](https://github.com/microsoft/mssql-jdbc/pull/1923) - Fixed concurrency issues in encrypt/decrypt obfuscation methods for truststore password [1968](https://github.com/microsoft/mssql-jdbc/pull/1968) - Fixed Idle Connection recovery so that unprocessedResponseCount isn't over decremented [1989](https://github.com/microsoft/mssql-jdbc/pull/1989) - Fixed race condition connecting to the wrong SQLServer host in configurable IPv6 [1968](https://github.com/microsoft/mssql-jdbc/pull/1968) @@ -184,7 +184,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) ### Fixed issues - Fixed double connection issue when enabling TDS 8.0 and SSL by reusing original socket connection [1817](https://github.com/microsoft/mssql-jdbc/pull/1817) - Fixed unknown token error 0xA3 when selectMethod cursor is used with data classification [1821](https://github.com/microsoft/mssql-jdbc/pull/1821) -- Fixed out of bounds error for when a data classification information type is not provided [1847](https://github.com/microsoft/mssql-jdbc/pull/1847) +- Fixed out-of-bounds error for when a data classification information type is not provided [1847](https://github.com/microsoft/mssql-jdbc/pull/1847) ## [11.1.1] Preview Release @@ -247,7 +247,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) ## [9.5.0] Preview Release ### Added - Idle Connection Resiliency Feature [1669](https://github.com/microsoft/mssql-jdbc/pull/1669) -- Fix for Bulkcopy multi byte characters in char/vchar columns [1671](https://github.com/microsoft/mssql-jdbc/pull/1671) +- Fix for Bulkcopy multibyte characters in char/vchar columns [1671](https://github.com/microsoft/mssql-jdbc/pull/1671) - Java 17 support [1676](https://github.com/microsoft/mssql-jdbc/pull/1676) - Added logging when deriving realm [1672](https://github.com/microsoft/mssql-jdbc/pull/1672) - Added check for closed statement to registerColumnEncryptionKeyStoreProvidersOnStatement [1644](https://github.com/microsoft/mssql-jdbc/pull/1644) @@ -776,12 +776,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) ## [6.3.2] Preview Release ### Added - Added new connection property: sslProtocol [#422](https://github.com/Microsoft/mssql-jdbc/pull/422) -- Added "slow" tag to long running tests [#461](https://github.com/Microsoft/mssql-jdbc/pull/461) +- Added "slow" tag to long-running tests [#461](https://github.com/Microsoft/mssql-jdbc/pull/461) ### Fixed Issues - Fixed some error messages [#452](https://github.com/Microsoft/mssql-jdbc/pull/452) & [#459](https://github.com/Microsoft/mssql-jdbc/pull/459) - Fixed statement leaks [#455](https://github.com/Microsoft/mssql-jdbc/pull/455) -- Fixed an issue regarding to loginTimeout with TLS [#456](https://github.com/Microsoft/mssql-jdbc/pull/456) +- Fixed an issue regarding loginTimeout with TLS [#456](https://github.com/Microsoft/mssql-jdbc/pull/456) - Fixed sql_variant issue with String type [#442](https://github.com/Microsoft/mssql-jdbc/pull/442) - Fixed issue with throwing error message for unsupported datatype [#450](https://github.com/Microsoft/mssql-jdbc/pull/450) - Fixed issue that initial batchException was not thrown [#458](https://github.com/Microsoft/mssql-jdbc/pull/458)