diff --git a/CHANGELOG.md b/CHANGELOG.md
index 945c12e34..e676970b6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,31 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
+## [12.8.0] Stable Release
+- No changes since previous release
+
+## [12.7.1] Preview Release
+### Added
+- Added JDK 22 support [#2414](https://github.com/microsoft/mssql-jdbc/pull/2414)
+- Added credential caching for Managed Identity Credential and Default Azure Credential [#2415](https://github.com/microsoft/mssql-jdbc/pull/2415)
+- Added Caching SQLServerBulkCopy object for batch insert [#2435](https://github.com/microsoft/mssql-jdbc/pull/2435)
+- Added connection level bulk copy metadata caching [#2464](https://github.com/microsoft/mssql-jdbc/pull/2464)
+- Added logging to token caching [#2468](https://github.com/microsoft/mssql-jdbc/pull/2468)
+
+### Changed
+- Bump org.bouncycastle:bcprov-jdk18on from 1.77 to 1.78 [#2403](https://github.com/microsoft/mssql-jdbc/pull/2403)
+- Enum SQLServerSortOrder is now public [#2405](https://github.com/microsoft/mssql-jdbc/pull/2405)
+- Bump com.azure:azure-identity from 1.12.1 to 1.12.2 [#2447](https://github.com/microsoft/mssql-jdbc/pull/2447)
+- Bump com.microsoft.azure:msal4j from 1.15.0 to 1.15.1 [#2448](https://github.com/microsoft/mssql-jdbc/pull/2448)
+
+### Fixed issues
+- Execute stored procedures directly for RPC calls [#2410](https://github.com/microsoft/mssql-jdbc/pull/2410)
+- Fix SqlAuthenticationToken constructor accepting unix epoch [#2425](https://github.com/microsoft/mssql-jdbc/pull/2425)
+- TokenCredential class shouldn't be required [#2441](https://github.com/microsoft/mssql-jdbc/pull/2441)
+- Fixed timestamp string conversion error for CallableStatements [#2449](https://github.com/microsoft/mssql-jdbc/pull/2449)
+- Fixed CallableStatements default value regression [#2452](https://github.com/microsoft/mssql-jdbc/pull/2452)
+- Fixed parentheses parsing for stored procedure names and function names [#2467](https://github.com/microsoft/mssql-jdbc/pull/2467)
+
## [12.7.0] Preview Release
### Added
- Server Message Handler and SQLException Chaining [#2251](https://github.com/microsoft/mssql-jdbc/pull/2251)
diff --git a/README.md b/README.md
index 6756d0e63..c99d951ae 100644
--- a/README.md
+++ b/README.md
@@ -83,7 +83,7 @@ We're now on the Maven Central Repository. Add the following to your POM file to
com.microsoft.sqlserver
mssql-jdbc
- 12.6.0.jre11
+ 12.8.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.
@@ -94,7 +94,7 @@ To get the latest version of the driver, add the following to your POM file:
com.microsoft.sqlserver
mssql-jdbc
- 12.6.0.jre11
+ 12.8.0.jre11
```
@@ -129,14 +129,14 @@ Projects that require either of the two features need to explicitly declare the
com.microsoft.sqlserver
mssql-jdbc
- 12.6.0.jre11
+ 12.8.0.jre11
compile
com.azure
azure-identity
- 1.3.3
+ 1.12.2
```
@@ -147,20 +147,20 @@ Projects that require either of the two features need to explicitly declare the
com.microsoft.sqlserver
mssql-jdbc
- 12.6.0.jre11
+ 12.8.0.jre11
compile
com.azure
azure-identity
- 1.3.3
+ 1.12.2
com.azure
azure-security-keyvault-keys
- 4.2.8
+ 4.7.3
```
@@ -174,13 +174,13 @@ When setting 'useFmtOnly' property to 'true' for establishing a connection or cr
com.microsoft.sqlserver
mssql-jdbc
- 12.6.0.jre11
+ 12.8.0.jre11
org.antlr
antlr4-runtime
- 4.9.2
+ 4.9.3
```
@@ -214,7 +214,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, 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.
+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, 12.8. 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, 12.7, and so on.
## Contributors
Special thanks to everyone who has contributed to the project.
diff --git a/build.gradle b/build.gradle
index 211eff493..8886d0d52 100644
--- a/build.gradle
+++ b/build.gradle
@@ -11,8 +11,8 @@
apply plugin: 'java'
-version = '12.7.1-SNAPSHOT'
-def releaseExt = '-preview'
+version = '12.8.0'
+def releaseExt = ''
def jreVersion = ""
def testOutputDir = file("build/classes/java/test")
def archivesBaseName = 'mssql-jdbc'
@@ -46,7 +46,7 @@ if (!hasProperty('buildProfile') || (hasProperty('buildProfile') && buildProfile
targetCompatibility = 22
}
-if (!hasProperty('buildProfile') || (hasProperty('buildProfile') && buildProfile == "jre21")) {
+if (hasProperty('buildProfile') && buildProfile == "jre21") {
jreVersion = "jre21"
excludedFile = 'com/microsoft/sqlserver/jdbc/SQLServerJdbc42.java'
@@ -143,29 +143,29 @@ repositories {
dependencies {
implementation 'org.osgi:org.osgi.core:6.0.0',
'org.osgi:org.osgi.service.jdbc:1.1.0'
- compileOnly 'com.azure:azure-security-keyvault-keys:4.5.2',
- 'com.azure:azure-identity:1.7.0',
+ compileOnly 'com.azure:azure-security-keyvault-keys:4.7.3',
+ 'com.azure:azure-identity:1.12.2',
'org.antlr:antlr4-runtime:4.9.3',
- 'com.google.code.gson:gson:2.9.0',
- 'org.bouncycastle:bcprov-jdk15on:1.70',
- 'org.bouncycastle:bcpkix-jdk15on:1.70'
+ 'com.google.code.gson:gson:2.10.1',
+ 'org.bouncycastle:bcprov-jdk18on:1.78',
+ 'org.bouncycastle:bcpkix-jdk18on:1.78'
testImplementation 'org.junit.platform:junit-platform-console:1.5.2',
'org.junit.platform:junit-platform-commons:1.5.2',
'org.junit.platform:junit-platform-engine:1.5.2',
'org.junit.platform:junit-platform-launcher:1.5.2',
'org.junit.platform:junit-platform-runner:1.5.2',
'org.junit.platform:junit-platform-surefire-provider:1.3.2',
- 'org.junit.jupiter:junit-jupiter-api:5.6.0',
- 'org.junit.jupiter:junit-jupiter-engine:5.6.0',
- 'org.junit.jupiter:junit-jupiter-params:5.6.0',
+ 'org.junit.jupiter:junit-jupiter-api:5.8.2',
+ 'org.junit.jupiter:junit-jupiter-engine:5.8.2',
+ 'org.junit.jupiter:junit-jupiter-params:5.8.2',
'com.zaxxer:HikariCP:3.4.2',
'org.apache.commons:commons-dbcp2:2.7.0',
'org.slf4j:slf4j-nop:1.7.30',
'org.antlr:antlr4-runtime:4.9.3',
'org.eclipse.gemini.blueprint:gemini-blueprint-mock:2.1.0.RELEASE',
- 'com.google.code.gson:gson:2.9.0',
- 'org.bouncycastle:bcprov-jdk15on:1.70',
- 'com.azure:azure-security-keyvault-keys:4.5.2',
- 'com.azure:azure-identity:1.7.0',
- 'com.h2database:h2:2.1.210'
+ 'com.google.code.gson:gson:2.10.1',
+ 'org.bouncycastle:bcprov-jdk18on:1.78',
+ 'com.azure:azure-security-keyvault-keys:4.7.3',
+ 'com.azure:azure-identity:1.12.2',
+ 'com.h2database:h2:2.2.220'
}
diff --git a/mssql-jdbc_auth_LICENSE b/mssql-jdbc_auth_LICENSE
index 7b1555e09..fad60a5d5 100644
--- a/mssql-jdbc_auth_LICENSE
+++ b/mssql-jdbc_auth_LICENSE
@@ -1,5 +1,5 @@
MICROSOFT SOFTWARE LICENSE TERMS
-MICROSOFT JDBC DRIVER 12.7.1 FOR SQL SERVER
+MICROSOFT JDBC DRIVER 12.8.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 42040419c..0e2c02ece 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
4.0.0
com.microsoft.sqlserver
mssql-jdbc
- 12.7.1-SNAPSHOT
+ 12.8.0
jar
Microsoft JDBC Driver for SQL Server
@@ -51,12 +51,12 @@
Default testing enabled with SQL Server 2019 (SQLv15) -->
xSQLv12,xSQLv15,NTLM,MSI,reqExternalSetup,clientCertAuth,fedAuth,kerberos
- -preview
+
6.0.0
4.7.3
- 1.12.1
- 1.15.0
+ 1.12.2
+ 1.15.1
1.1.0
4.9.3
2.10.1
@@ -347,9 +347,6 @@
jre21
-
- true
-
${project.artifactId}-${project.version}.jre21${releaseExt}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
index 2479e5063..bd22c1fc9 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
@@ -532,4 +532,19 @@ CallableStatement prepareCall(String sql, int nType, int nConcur, int nHold,
* @return flag for using Bulk Copy API for batch insert operations.
*/
boolean getUseBulkCopyForBatchInsert();
+
+ /**
+ * Returns value of 'cacheBulkCopyMetadata' from Connection String.
+ *
+ * @param cacheBulkCopyMetadata
+ * indicates whether the driver should use connection level caching of metadata for bulk copy
+ */
+ void setcacheBulkCopyMetadata(boolean cacheBulkCopyMetadata);
+
+ /**
+ * Sets the value for 'cacheBulkCopyMetadata' property
+ *
+ * @return cacheBulkCopyMetadata boolean value
+ */
+ boolean getcacheBulkCopyMetadata();
}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
index 823b11d9d..0792ebd9c 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
@@ -1350,4 +1350,19 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource {
* @return calcBigDecimalPrecision boolean value
*/
boolean getCalcBigDecimalPrecision();
+
+ /**
+ * Returns value of 'cacheBulkCopyMetadata' from Connection String.
+ *
+ * @param cacheBulkCopyMetadata
+ * indicates whether the driver should use connection level caching of metadata for bulk copy
+ */
+ void setcacheBulkCopyMetadata(boolean cacheBulkCopyMetadata);
+
+ /**
+ * Sets the value for 'cacheBulkCopyMetadata' property
+ *
+ * @return cacheBulkCopyMetadata boolean value
+ */
+ boolean getcacheBulkCopyMetadata();
}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java
index 8a975e539..e246c1844 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java
@@ -7,15 +7,15 @@
final class SQLJdbcVersion {
static final int MAJOR = 12;
- static final int MINOR = 7;
- static final int PATCH = 1;
+ static final int MINOR = 8;
+ static final int PATCH = 0;
static final int BUILD = 0;
/*
* Used to load mssql-jdbc_auth DLL.
* 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/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java
index b868ba5fd..199c15306 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java
@@ -5,6 +5,8 @@
package com.microsoft.sqlserver.jdbc;
+import static com.microsoft.sqlserver.jdbc.SQLServerConnection.BULK_COPY_OPERATION_CACHE;
+import static com.microsoft.sqlserver.jdbc.Util.getHashedSecret;
import static java.nio.charset.StandardCharsets.UTF_16LE;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -17,6 +19,8 @@
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.sql.Connection;
import java.sql.Date;
import java.sql.ResultSet;
@@ -46,6 +50,8 @@
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import javax.sql.RowSet;
@@ -263,6 +269,11 @@ class BulkColumnMetaData {
*/
private transient ScheduledFuture> timeout;
+ /**
+ * Shared timer
+ */
+ private static final Lock DESTINATION_COL_METADATA_LOCK = new ReentrantLock();
+
/**
* The maximum temporal precision we can send when using varchar(precision) in bulkcommand, to send a
* smalldatetime/datetime value.
@@ -1719,61 +1730,115 @@ private void getDestinationMetadata() throws SQLServerException {
}
String escapedDestinationTableName = Util.escapeSingleQuotes(destinationTableName);
+ String key = null;
+
+ if (connection.getcacheBulkCopyMetadata()) {
+ String databaseName = connection.activeConnectionProperties
+ .getProperty(SQLServerDriverStringProperty.DATABASE_NAME.toString());
+ key = getHashedSecret(new String[] {escapedDestinationTableName, databaseName});
+ destColumnMetadata = BULK_COPY_OPERATION_CACHE.get(key);
+ }
+
+ if (null == destColumnMetadata || destColumnMetadata.isEmpty()) {
+ if (connection.getcacheBulkCopyMetadata()) {
+ DESTINATION_COL_METADATA_LOCK.lock();
+ destColumnMetadata = BULK_COPY_OPERATION_CACHE.get(key);
+
+ if (null == destColumnMetadata || destColumnMetadata.isEmpty()) {
+ try {
+ setDestinationColumnMetadata(escapedDestinationTableName);
+
+ // We are caching the following metadata about the table:
+ // 1. collation_name
+ // 2. is_computed
+ // 3. encryption_type
+ //
+ // Using this caching method, 'cacheBulkCopyMetadata', may have unintended consequences if the
+ // table changes somehow between inserts. For example, if the collation_name changes, the
+ // driver will not be aware of this and the inserted data will likely be corrupted. In such
+ // scenario, we can't detect this without making an additional metadata query, which would
+ // defeat the purpose of caching.
+ BULK_COPY_OPERATION_CACHE.put(key, destColumnMetadata);
+ } finally {
+ DESTINATION_COL_METADATA_LOCK.unlock();
+ }
+ }
+ if (loggerExternal.isLoggable(Level.FINER)) {
+ loggerExternal.finer(this.toString() + " Acquiring existing destination column metadata " +
+ "from cache for bulk copy");
+ }
+
+ } else {
+ setDestinationColumnMetadata(escapedDestinationTableName);
+
+ if (loggerExternal.isLoggable(Level.FINER)) {
+ loggerExternal.finer(this.toString() + " cacheBulkCopyMetadata=false - Querying server " +
+ "for destination column metadata");
+ }
+ }
+ }
+
+ destColumnCount = destColumnMetadata.size();
+ }
+
+ private void setDestinationColumnMetadata(String escapedDestinationTableName) throws SQLServerException {
SQLServerResultSet rs = null;
SQLServerStatement stmt = null;
String metaDataQuery = null;
- if (null == destColumnMetadata || destColumnMetadata.isEmpty()) {
- try {
- if (null != destinationTableMetadata) {
- rs = (SQLServerResultSet) destinationTableMetadata;
- } else {
- stmt = (SQLServerStatement) connection.createStatement(ResultSet.TYPE_FORWARD_ONLY,
- ResultSet.CONCUR_READ_ONLY, connection.getHoldability(), stmtColumnEncriptionSetting);
+ try {
+ if (null != destinationTableMetadata) {
+ rs = (SQLServerResultSet) destinationTableMetadata;
+ } else {
+ stmt = (SQLServerStatement) connection.createStatement(ResultSet.TYPE_FORWARD_ONLY,
+ ResultSet.CONCUR_READ_ONLY, connection.getHoldability(), stmtColumnEncriptionSetting);
- // Get destination metadata
- rs = stmt.executeQueryInternal(
- "sp_executesql N'SET FMTONLY ON SELECT * FROM " + escapedDestinationTableName + " '");
- }
+ // Get destination metadata
+ rs = stmt.executeQueryInternal(
+ "sp_executesql N'SET FMTONLY ON SELECT * FROM " + escapedDestinationTableName + " '");
+ }
- int destColumnMetadataCount = rs.getMetaData().getColumnCount();
- destColumnMetadata = new HashMap<>();
- destCekTable = rs.getCekTable();
+ int destColumnMetadataCount = rs.getMetaData().getColumnCount();
+ destColumnMetadata = new HashMap<>();
+ destCekTable = rs.getCekTable();
- metaDataQuery = "select * from sys.columns where " + "object_id=OBJECT_ID('"
+ if (!connection.getServerSupportsColumnEncryption()) {
+ metaDataQuery = "select collation_name, is_computed from sys.columns where " + "object_id=OBJECT_ID('"
+ escapedDestinationTableName + "') " + "order by column_id ASC";
+ } else {
+ metaDataQuery = "select collation_name, is_computed, encryption_type from sys.columns where "
+ + "object_id=OBJECT_ID('" + escapedDestinationTableName + "') " + "order by column_id ASC";
+ }
- try (SQLServerStatement statementMoreMetadata = (SQLServerStatement) connection.createStatement();
- SQLServerResultSet rsMoreMetaData = statementMoreMetadata.executeQueryInternal(metaDataQuery)) {
- for (int i = 1; i <= destColumnMetadataCount; ++i) {
- if (rsMoreMetaData.next()) {
- String bulkCopyEncryptionType = null;
- if (connection.getServerSupportsColumnEncryption()) {
- bulkCopyEncryptionType = rsMoreMetaData.getString("encryption_type");
- }
- // Skip computed columns
- if (!rsMoreMetaData.getBoolean("is_computed")) {
- destColumnMetadata.put(i, new BulkColumnMetaData(rs.getColumn(i),
- rsMoreMetaData.getString("collation_name"), bulkCopyEncryptionType));
- }
- } else {
- destColumnMetadata.put(i, new BulkColumnMetaData(rs.getColumn(i)));
+ try (SQLServerStatement statementMoreMetadata = (SQLServerStatement) connection.createStatement();
+ SQLServerResultSet rsMoreMetaData = statementMoreMetadata.executeQueryInternal(metaDataQuery)) {
+ for (int i = 1; i <= destColumnMetadataCount; ++i) {
+ if (rsMoreMetaData.next()) {
+ String bulkCopyEncryptionType = null;
+ if (connection.getServerSupportsColumnEncryption()) {
+ bulkCopyEncryptionType = rsMoreMetaData.getString("encryption_type");
}
+ // Skip computed columns
+ if (!rsMoreMetaData.getBoolean("is_computed")) {
+ destColumnMetadata.put(i, new BulkColumnMetaData(rs.getColumn(i),
+ rsMoreMetaData.getString("collation_name"), bulkCopyEncryptionType));
+ }
+ } else {
+ destColumnMetadata.put(i, new BulkColumnMetaData(rs.getColumn(i)));
}
- destColumnCount = destColumnMetadata.size();
- }
- } catch (SQLException e) {
- // Unable to retrieve metadata for destination
- throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveColMeta"), e);
- } finally {
- if (null != rs) {
- rs.close();
- }
- if (null != stmt) {
- stmt.close();
}
}
+ } catch (SQLException e) {
+ // Unable to retrieve metadata for destination
+ throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveColMeta"), e);
+ } finally {
+ if (null != rs) {
+ rs.close();
+ }
+ if (null != stmt) {
+ stmt.close();
+ }
}
}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
index 48275d2aa..492dd360f 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
@@ -137,6 +137,9 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
/** Current limit for this particular connection. */
private Boolean enablePrepareOnFirstPreparedStatementCall = null;
+ /** Used for toggling bulk copy caching */
+ private Boolean cacheBulkCopyMetadata = null;
+
/** Used for toggling use of sp_prepare */
private String prepareMethod = null;
@@ -1552,6 +1555,8 @@ public static void clearUserTokenCache() {
/** transaction descriptor */
private byte[] transactionDescriptor = new byte[8];
+ static final HashMap> BULK_COPY_OPERATION_CACHE = new HashMap<>();
+
/**
* Flag (Yukon and later) set to true whenever a transaction is rolled back..The flag's value is reset to false when
* a new transaction starts or when the autoCommit mode changes.
@@ -3044,6 +3049,12 @@ else if (0 == requestedPacketSize)
useBulkCopyForBatchInsert = isBooleanPropertyOn(sPropKey, sPropValue);
}
+ sPropKey = SQLServerDriverBooleanProperty.ENABLE_BULK_COPY_CACHE.toString();
+ sPropValue = activeConnectionProperties.getProperty(sPropKey);
+ if (null != sPropValue) {
+ setcacheBulkCopyMetadata(isBooleanPropertyOn(sPropKey, sPropValue));
+ }
+
sPropKey = SQLServerDriverStringProperty.SSL_PROTOCOL.toString();
sPropValue = activeConnectionProperties.getProperty(sPropKey);
if (null == sPropValue) {
@@ -8050,6 +8061,20 @@ public void setEnablePrepareOnFirstPreparedStatementCall(boolean value) {
this.enablePrepareOnFirstPreparedStatementCall = value;
}
+ @Override
+ public boolean getcacheBulkCopyMetadata() {
+ if (null == this.cacheBulkCopyMetadata) {
+ return false;
+ }
+
+ return this.cacheBulkCopyMetadata;
+ }
+
+ @Override
+ public void setcacheBulkCopyMetadata(boolean value) {
+ this.cacheBulkCopyMetadata = value;
+ }
+
@Override
public String getPrepareMethod() {
if (null == this.prepareMethod) {
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
index 899f10595..d9eb047ea 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
@@ -562,6 +562,16 @@ public void setEnablePrepareOnFirstPreparedStatementCall(boolean value) {
wrappedConnection.setEnablePrepareOnFirstPreparedStatementCall(value);
}
+ @Override
+ public boolean getcacheBulkCopyMetadata() {
+ return wrappedConnection.getcacheBulkCopyMetadata();
+ }
+
+ @Override
+ public void setcacheBulkCopyMetadata(boolean value) {
+ wrappedConnection.setcacheBulkCopyMetadata(value);
+ }
+
@Override
public String getPrepareMethod() {
return wrappedConnection.getPrepareMethod();
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
index 7dfffdb51..7d122c1e1 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
@@ -959,6 +959,21 @@ public boolean getEnablePrepareOnFirstPreparedStatementCall() {
SQLServerDriverBooleanProperty.ENABLE_PREPARE_ON_FIRST_PREPARED_STATEMENT.toString(), defaultValue);
}
+ @Override
+ public void setcacheBulkCopyMetadata(boolean cacheBulkCopyMetadata) {
+ setBooleanProperty(connectionProps,
+ SQLServerDriverBooleanProperty.ENABLE_BULK_COPY_CACHE.toString(),
+ cacheBulkCopyMetadata);
+ }
+
+ @Override
+ public boolean getcacheBulkCopyMetadata() {
+ boolean defaultValue = SQLServerDriverBooleanProperty.ENABLE_BULK_COPY_CACHE
+ .getDefaultValue();
+ return getBooleanProperty(connectionProps,
+ SQLServerDriverBooleanProperty.ENABLE_BULK_COPY_CACHE.toString(), defaultValue);
+ }
+
@Override
public void setServerPreparedStatementDiscardThreshold(int serverPreparedStatementDiscardThreshold) {
setIntProperty(connectionProps,
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
index 2c45d1090..4c4c0724b 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
@@ -690,6 +690,7 @@ enum SQLServerDriverBooleanProperty {
XOPEN_STATES("xopenStates", false),
FIPS("fips", false),
ENABLE_PREPARE_ON_FIRST_PREPARED_STATEMENT("enablePrepareOnFirstPreparedStatementCall", SQLServerConnection.DEFAULT_ENABLE_PREPARE_ON_FIRST_PREPARED_STATEMENT_CALL),
+ ENABLE_BULK_COPY_CACHE("cacheBulkCopyMetadata", false),
USE_BULK_COPY_FOR_BATCH_INSERT("useBulkCopyForBatchInsert", false),
USE_FMT_ONLY("useFmtOnly", false),
SEND_TEMPORAL_DATATYPES_AS_STRING_FOR_BULK_COPY("sendTemporalDataTypesAsStringForBulkCopy", true),
@@ -918,6 +919,9 @@ public final class SQLServerDriver implements java.sql.Driver {
new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT.toString(),
Boolean.toString(SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT.getDefaultValue()),
false, TRUE_FALSE),
+ new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.ENABLE_BULK_COPY_CACHE.toString(),
+ Boolean.toString(SQLServerDriverBooleanProperty.ENABLE_BULK_COPY_CACHE.getDefaultValue()),
+ false, TRUE_FALSE),
new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.MSI_CLIENT_ID.toString(),
SQLServerDriverStringProperty.MSI_CLIENT_ID.getDefaultValue(), false, null),
new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.KEY_VAULT_PROVIDER_CLIENT_ID.toString(),
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java
index 1ae69891b..8c79202b0 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java
@@ -14,7 +14,6 @@
import java.net.URI;
import java.net.URISyntaxException;
-import java.security.MessageDigest;
import java.text.MessageFormat;
import java.util.Collections;
@@ -26,6 +25,7 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
@@ -56,6 +56,8 @@
import com.microsoft.sqlserver.jdbc.SQLServerConnection.ActiveDirectoryAuthentication;
import com.microsoft.sqlserver.jdbc.SQLServerConnection.SqlFedAuthInfo;
+import static com.microsoft.sqlserver.jdbc.Util.getHashedSecret;
+
class SQLServerMSAL4JUtils {
@@ -89,11 +91,21 @@ static SqlAuthenticationToken getSqlFedAuthToken(SqlFedAuthInfo fedAuthInfo, Str
try {
String hashedSecret = getHashedSecret(new String[] {fedAuthInfo.stsurl, user, password});
- PersistentTokenCacheAccessAspect persistentTokenCacheAccessAspect = TOKEN_CACHE_MAP.getEntry(hashedSecret);
+ PersistentTokenCacheAccessAspect persistentTokenCacheAccessAspect = TOKEN_CACHE_MAP.getEntry(user,
+ hashedSecret);
+ // check if account password was changed
if (null == persistentTokenCacheAccessAspect) {
persistentTokenCacheAccessAspect = new PersistentTokenCacheAccessAspect();
TOKEN_CACHE_MAP.addEntry(hashedSecret, persistentTokenCacheAccessAspect);
+
+ if (logger.isLoggable(Level.FINEST)) {
+ logger.finest(LOGCONTEXT + ": cache token for user: " + user);
+ }
+ } else {
+ if (logger.isLoggable(Level.FINEST)) {
+ logger.finest(LOGCONTEXT + ": retrieved cached token for user: " + user);
+ }
}
final PublicClientApplication pca = PublicClientApplication
@@ -145,11 +157,21 @@ static SqlAuthenticationToken getSqlFedAuthTokenPrincipal(SqlFedAuthInfo fedAuth
try {
String hashedSecret = getHashedSecret(
new String[] {fedAuthInfo.stsurl, aadPrincipalID, aadPrincipalSecret});
- PersistentTokenCacheAccessAspect persistentTokenCacheAccessAspect = TOKEN_CACHE_MAP.getEntry(hashedSecret);
+ PersistentTokenCacheAccessAspect persistentTokenCacheAccessAspect = TOKEN_CACHE_MAP.getEntry(aadPrincipalID,
+ hashedSecret);
+ // check if principal secret was changed
if (null == persistentTokenCacheAccessAspect) {
persistentTokenCacheAccessAspect = new PersistentTokenCacheAccessAspect();
TOKEN_CACHE_MAP.addEntry(hashedSecret, persistentTokenCacheAccessAspect);
+
+ if (logger.isLoggable(Level.FINEST)) {
+ logger.finest(LOGCONTEXT + ": cache token for principal id: " + aadPrincipalID);
+ }
+ } else {
+ if (logger.isLoggable(Level.FINEST)) {
+ logger.finest(LOGCONTEXT + ": retrieved cached token for principal id: " + aadPrincipalID);
+ }
}
IClientCredential credential = ClientCredentialFactory.createFromSecret(aadPrincipalSecret);
@@ -202,11 +224,21 @@ static SqlAuthenticationToken getSqlFedAuthTokenPrincipalCertificate(SqlFedAuthI
try {
String hashedSecret = getHashedSecret(new String[] {fedAuthInfo.stsurl, aadPrincipalID, certFile,
certPassword, certKey, certKeyPassword});
- PersistentTokenCacheAccessAspect persistentTokenCacheAccessAspect = TOKEN_CACHE_MAP.getEntry(hashedSecret);
+ PersistentTokenCacheAccessAspect persistentTokenCacheAccessAspect = TOKEN_CACHE_MAP.getEntry(aadPrincipalID,
+ hashedSecret);
+ // check if cert was changed
if (null == persistentTokenCacheAccessAspect) {
persistentTokenCacheAccessAspect = new PersistentTokenCacheAccessAspect();
TOKEN_CACHE_MAP.addEntry(hashedSecret, persistentTokenCacheAccessAspect);
+
+ if (logger.isLoggable(Level.FINEST)) {
+ logger.finest(LOGCONTEXT + ": cache token for principal id: " + aadPrincipalID);
+ }
+ } else {
+ if (logger.isLoggable(Level.FINEST)) {
+ logger.finest(LOGCONTEXT + ": retrieved cached token for principal id: " + aadPrincipalID);
+ }
}
ConfidentialClientApplication clientApplication = null;
@@ -476,35 +508,28 @@ private static SQLServerException getCorrectedException(Exception e, String user
}
}
- 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 getEntry(String value, String key) {
PersistentTokenCacheAccessAspect persistentTokenCacheAccessAspect = tokenCacheMap.get(key);
if (null != persistentTokenCacheAccessAspect) {
- if (System.currentTimeMillis() > persistentTokenCacheAccessAspect.getExpiryTime()) {
+ long currentTime = System.currentTimeMillis();
+
+ if (currentTime > persistentTokenCacheAccessAspect.getExpiryTime()) {
tokenCacheMap.remove(key);
persistentTokenCacheAccessAspect = new PersistentTokenCacheAccessAspect();
persistentTokenCacheAccessAspect
- .setExpiryTime(System.currentTimeMillis() + PersistentTokenCacheAccessAspect.TIME_TO_LIVE);
+ .setExpiryTime(currentTime + PersistentTokenCacheAccessAspect.TIME_TO_LIVE);
tokenCacheMap.put(key, persistentTokenCacheAccessAspect);
+
+ if (logger.isLoggable(Level.FINEST)) {
+ logger.finest(LOGCONTEXT + ": entry expired for: " + value + " new entry will expire in: "
+ + TimeUnit.MILLISECONDS.toSeconds(PersistentTokenCacheAccessAspect.TIME_TO_LIVE) + "s");
+ }
}
}
@@ -514,6 +539,10 @@ PersistentTokenCacheAccessAspect getEntry(String key) {
void addEntry(String key, PersistentTokenCacheAccessAspect value) {
value.setExpiryTime(System.currentTimeMillis() + PersistentTokenCacheAccessAspect.TIME_TO_LIVE);
tokenCacheMap.put(key, value);
+ if (logger.isLoggable(Level.FINEST)) {
+ logger.finest(LOGCONTEXT + ": add entry for: " + value + ", will expire in: "
+ + TimeUnit.MILLISECONDS.toSeconds(PersistentTokenCacheAccessAspect.TIME_TO_LIVE) + "s");
+ }
}
}
}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java
index f3a441581..050f77cfd 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java
@@ -139,13 +139,21 @@ private void setPreparedStatementHandle(int handle) {
* Regex for JDBC 'call' escape syntax
*/
private static final Pattern callEscapePattern = Pattern
- .compile("^\\s*(?i)\\{(\\s*\\??\\s*=?\\s*)call (.+)\\s*\\(?\\?*,?\\)?\\s*}\\s*$");
+ .compile("^\\s*(?i)\\{(\\s*\\??\\s*=?\\s*)call [^\\(\\)]+\\s*((\\(\\s*\\?\\s*(,\\s*\\?\\s*)*\\))?|\\(\\))\\s*}");
/**
* Regex for 'exec' escape syntax
*/
private static final Pattern execEscapePattern = Pattern.compile("^\\s*(?i)(?:exec|execute)\\b");
+ /**
+ * For caching data related to batch insert with bulkcopy
+ */
+ private SQLServerBulkCopy bcOperation = null;
+ private String bcOperationTableName = null;
+ private ArrayList bcOperationColumnList = null;
+ private ArrayList bcOperationValueList = null;
+
/** Returns the prepared statement SQL */
@Override
public String toString() {
@@ -392,6 +400,10 @@ final void closeInternal() {
// Clean up client-side state
batchParamValues = null;
+
+ if (null != bcOperation) {
+ bcOperation.close();
+ }
}
/**
@@ -2287,9 +2299,17 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL
}
}
- String tableName = parseUserSQLForTableNameDW(false, false, false, false);
- ArrayList columnList = parseUserSQLForColumnListDW();
- ArrayList valueList = parseUserSQLForValueListDW(false);
+ if (null == bcOperationTableName) {
+ bcOperationTableName = parseUserSQLForTableNameDW(false, false, false, false);
+ }
+
+ if (null == bcOperationColumnList) {
+ bcOperationColumnList = parseUserSQLForColumnListDW();
+ }
+
+ if (null == bcOperationValueList) {
+ bcOperationValueList = parseUserSQLForValueListDW(false);
+ }
checkAdditionalQuery();
@@ -2298,28 +2318,28 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL
stmtColumnEncriptionSetting);
SQLServerResultSet rs = stmt
.executeQueryInternal("sp_executesql N'SET FMTONLY ON SELECT * FROM "
- + Util.escapeSingleQuotes(tableName) + " '")) {
+ + Util.escapeSingleQuotes(bcOperationTableName) + " '")) {
Map columnMappings = null;
- if (null != columnList && !columnList.isEmpty()) {
- if (columnList.size() != valueList.size()) {
+ if (null != bcOperationColumnList && !bcOperationColumnList.isEmpty()) {
+ if (bcOperationColumnList.size() != bcOperationValueList.size()) {
MessageFormat form = new MessageFormat(
SQLServerException.getErrString("R_colNotMatchTable"));
- Object[] msgArgs = {columnList.size(), valueList.size()};
+ Object[] msgArgs = {bcOperationColumnList.size(), bcOperationValueList.size()};
throw new IllegalArgumentException(form.format(msgArgs));
}
- columnMappings = new HashMap<>(columnList.size());
+ columnMappings = new HashMap<>(bcOperationColumnList.size());
} else {
- if (rs.getColumnCount() != valueList.size()) {
+ if (rs.getColumnCount() != bcOperationValueList.size()) {
MessageFormat form = new MessageFormat(
SQLServerException.getErrString("R_colNotMatchTable"));
- Object[] msgArgs = {rs.getColumnCount(), valueList.size()};
+ Object[] msgArgs = {rs.getColumnCount(), bcOperationValueList.size()};
throw new IllegalArgumentException(form.format(msgArgs));
}
}
SQLServerBulkBatchInsertRecord batchRecord = new SQLServerBulkBatchInsertRecord(
- batchParamValues, columnList, valueList, null);
+ batchParamValues, bcOperationColumnList, bcOperationValueList, null);
for (int i = 1; i <= rs.getColumnCount(); i++) {
Column c = rs.getColumn(i);
@@ -2335,8 +2355,8 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL
} else {
jdbctype = ti.getSSType().getJDBCType().getIntValue();
}
- if (null != columnList && !columnList.isEmpty()) {
- int columnIndex = columnList.indexOf(c.getColumnName());
+ if (null != bcOperationColumnList && !bcOperationColumnList.isEmpty()) {
+ int columnIndex = bcOperationColumnList.indexOf(c.getColumnName());
if (columnIndex > -1) {
columnMappings.put(columnIndex + 1, i);
batchRecord.addColumnMetadata(columnIndex + 1, c.getColumnName(), jdbctype,
@@ -2348,20 +2368,23 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL
}
}
- SQLServerBulkCopy bcOperation = new SQLServerBulkCopy(connection);
- SQLServerBulkCopyOptions option = new SQLServerBulkCopyOptions();
- option.setBulkCopyTimeout(queryTimeout);
- bcOperation.setBulkCopyOptions(option);
- bcOperation.setDestinationTableName(tableName);
- if (columnMappings != null) {
- for (Entry pair : columnMappings.entrySet()) {
- bcOperation.addColumnMapping(pair.getKey(), pair.getValue());
+ if (null == bcOperation) {
+ bcOperation = new SQLServerBulkCopy(connection);
+ SQLServerBulkCopyOptions option = new SQLServerBulkCopyOptions();
+ option.setBulkCopyTimeout(queryTimeout);
+ bcOperation.setBulkCopyOptions(option);
+ bcOperation.setDestinationTableName(bcOperationTableName);
+ if (columnMappings != null) {
+ for (Entry pair : columnMappings.entrySet()) {
+ bcOperation.addColumnMapping(pair.getKey(), pair.getValue());
+ }
}
+ bcOperation.setStmtColumnEncriptionSetting(this.getStmtColumnEncriptionSetting());
+ bcOperation.setDestinationTableMetadata(rs);
}
- bcOperation.setStmtColumnEncriptionSetting(this.getStmtColumnEncriptionSetting());
- bcOperation.setDestinationTableMetadata(rs);
+
bcOperation.writeToServer(batchRecord);
- bcOperation.close();
+
updateCounts = new int[batchParamValues.size()];
for (int i = 0; i < batchParamValues.size(); ++i) {
updateCounts[i] = 1;
@@ -2471,9 +2494,17 @@ public long[] executeLargeBatch() throws SQLServerException, BatchUpdateExceptio
}
}
- String tableName = parseUserSQLForTableNameDW(false, false, false, false);
- ArrayList columnList = parseUserSQLForColumnListDW();
- ArrayList valueList = parseUserSQLForValueListDW(false);
+ if (null == bcOperationTableName) {
+ bcOperationTableName = parseUserSQLForTableNameDW(false, false, false, false);
+ }
+
+ if (null == bcOperationColumnList) {
+ bcOperationColumnList = parseUserSQLForColumnListDW();
+ }
+
+ if (null == bcOperationValueList) {
+ bcOperationValueList = parseUserSQLForValueListDW(false);
+ }
checkAdditionalQuery();
@@ -2482,25 +2513,25 @@ public long[] executeLargeBatch() throws SQLServerException, BatchUpdateExceptio
stmtColumnEncriptionSetting);
SQLServerResultSet rs = stmt
.executeQueryInternal("sp_executesql N'SET FMTONLY ON SELECT * FROM "
- + Util.escapeSingleQuotes(tableName) + " '")) {
- if (null != columnList && !columnList.isEmpty()) {
- if (columnList.size() != valueList.size()) {
+ + Util.escapeSingleQuotes(bcOperationTableName) + " '")) {
+ if (null != bcOperationColumnList && !bcOperationColumnList.isEmpty()) {
+ if (bcOperationColumnList.size() != bcOperationValueList.size()) {
MessageFormat form = new MessageFormat(
SQLServerException.getErrString("R_colNotMatchTable"));
- Object[] msgArgs = {columnList.size(), valueList.size()};
+ Object[] msgArgs = {bcOperationColumnList.size(), bcOperationValueList.size()};
throw new IllegalArgumentException(form.format(msgArgs));
}
} else {
- if (rs.getColumnCount() != valueList.size()) {
+ if (rs.getColumnCount() != bcOperationValueList.size()) {
MessageFormat form = new MessageFormat(
SQLServerException.getErrString("R_colNotMatchTable"));
- Object[] msgArgs = {columnList != null ? columnList.size() : 0, valueList.size()};
+ Object[] msgArgs = {bcOperationColumnList!= null ? bcOperationColumnList.size() : 0, bcOperationValueList.size()};
throw new IllegalArgumentException(form.format(msgArgs));
}
}
SQLServerBulkBatchInsertRecord batchRecord = new SQLServerBulkBatchInsertRecord(
- batchParamValues, columnList, valueList, null);
+ batchParamValues, bcOperationColumnList, bcOperationValueList, null);
for (int i = 1; i <= rs.getColumnCount(); i++) {
Column c = rs.getColumn(i);
@@ -2517,15 +2548,18 @@ public long[] executeLargeBatch() throws SQLServerException, BatchUpdateExceptio
ti.getScale());
}
- SQLServerBulkCopy bcOperation = new SQLServerBulkCopy(connection);
- SQLServerBulkCopyOptions option = new SQLServerBulkCopyOptions();
- option.setBulkCopyTimeout(queryTimeout);
- bcOperation.setBulkCopyOptions(option);
- bcOperation.setDestinationTableName(tableName);
- bcOperation.setStmtColumnEncriptionSetting(this.getStmtColumnEncriptionSetting());
- bcOperation.setDestinationTableMetadata(rs);
+ if (null == bcOperation) {
+ bcOperation = new SQLServerBulkCopy(connection);
+ SQLServerBulkCopyOptions option = new SQLServerBulkCopyOptions();
+ option.setBulkCopyTimeout(queryTimeout);
+ bcOperation.setBulkCopyOptions(option);
+ bcOperation.setDestinationTableName(bcOperationTableName);
+ bcOperation.setStmtColumnEncriptionSetting(this.getStmtColumnEncriptionSetting());
+ bcOperation.setDestinationTableMetadata(rs);
+ }
+
bcOperation.writeToServer(batchRecord);
- bcOperation.close();
+
updateCounts = new long[batchParamValues.size()];
for (int i = 0; i < batchParamValues.size(); ++i) {
updateCounts[i] = 1;
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
index 28a1b219a..246c6cf60 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
@@ -234,6 +234,7 @@ protected Object[][] getContents() {
{"R_socketTimeoutPropertyDescription", "The number of milliseconds to wait before the java.net.SocketTimeoutException is raised."},
{"R_serverPreparedStatementDiscardThresholdPropertyDescription", "The threshold for when to close discarded prepare statements on the server (calling a batch of sp_unprepares). A value of 1 or less will cause sp_unprepare to be called immediately on PreparedStatment close."},
{"R_enablePrepareOnFirstPreparedStatementCallPropertyDescription", "This setting specifies whether a prepared statement is prepared (sp_prepexec) on first use (property=true) or on second after first calling sp_executesql (property=false)."},
+ {"R_cacheBulkCopyMetadataPropertyDescription", "This setting specifies whether the driver caches the metadata used for bulk copy for batch inserts."},
{"R_statementPoolingCacheSizePropertyDescription", "This setting specifies the size of the prepared statement cache for a connection. A value less than 1 means no cache."},
{"R_gsscredentialPropertyDescription", "Impersonated GSS Credential to access SQL Server."},
{"R_msiClientIdPropertyDescription", "Client Id of User Assigned Managed Identity to be used for generating access token for Azure AD MSI Authentication"},
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSecurityUtility.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSecurityUtility.java
index b49abe9bc..70c50ca28 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSecurityUtility.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSecurityUtility.java
@@ -6,7 +6,6 @@
package com.microsoft.sqlserver.jdbc;
import java.security.InvalidKeyException;
-import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.MessageFormat;
import java.util.Arrays;
@@ -21,13 +20,14 @@
import javax.crypto.spec.SecretKeySpec;
import com.azure.core.credential.AccessToken;
-import com.azure.core.credential.TokenCredential;
import com.azure.core.credential.TokenRequestContext;
import com.azure.identity.ManagedIdentityCredential;
import com.azure.identity.ManagedIdentityCredentialBuilder;
import com.azure.identity.DefaultAzureCredential;
import com.azure.identity.DefaultAzureCredentialBuilder;
+import static com.microsoft.sqlserver.jdbc.Util.getHashedSecret;
+
/**
* Various SQLServer security utilities.
@@ -487,7 +487,7 @@ private static String[] getAdditonallyAllowedTenants() {
return null;
}
- private static TokenCredential getCredentialFromCache(String key) {
+ private static Object getCredentialFromCache(String key) {
Credential credential = CREDENTIAL_CACHE.get(key);
if (null != credential) {
@@ -498,24 +498,10 @@ private static TokenCredential getCredentialFromCache(String key) {
}
private static class Credential {
- TokenCredential tokenCredential;
+ Object tokenCredential;
- public Credential(TokenCredential tokenCredential) {
+ public Credential(Object tokenCredential) {
this.tokenCredential = tokenCredential;
}
}
-
- 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);
- }
- }
}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java
index 61bda22b7..374626cbe 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java
@@ -11,6 +11,8 @@
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.UnknownHostException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
import java.text.MessageFormat;
import java.util.Date;
@@ -1044,6 +1046,20 @@ static char[] bytesToChars(byte[] bytes) {
}
return chars;
}
+
+ 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);
+ }
+ }
}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java
index e0db03608..81410fc4b 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java
@@ -24,6 +24,7 @@
import java.sql.Blob;
import java.sql.Clob;
import java.sql.SQLException;
+import java.sql.Timestamp;
import java.text.MessageFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
@@ -1643,6 +1644,8 @@ final void executeOp(DTVExecuteOp op) throws SQLServerException {
op.execute(this, ((Geometry) value).serialize());
} else if (JDBCType.GEOGRAPHY == jdbcType) {
op.execute(this, ((Geography) value).serialize());
+ } else if (JDBCType.TIMESTAMP == jdbcType) {
+ op.execute(this, Timestamp.valueOf((String) value));
} else {
if (null != cryptoMeta) {
// if streaming types check for allowed data length in AE
diff --git a/src/samples/adaptive/pom.xml b/src/samples/adaptive/pom.xml
index f1ce6c7d4..f185669c6 100644
--- a/src/samples/adaptive/pom.xml
+++ b/src/samples/adaptive/pom.xml
@@ -1,4 +1,4 @@
-
4.0.0
@@ -15,7 +15,7 @@
com.microsoft.sqlserver
mssql-jdbc
- 12.6.0.jre11
+ 12.8.0.jre11
@@ -74,8 +74,8 @@
org.apache.maven.plugins
maven-compiler-plugin
- 18
- 18
+ 22
+ 22
@@ -85,4 +85,4 @@
-
+ -->
diff --git a/src/samples/alwaysencrypted/pom.xml b/src/samples/alwaysencrypted/pom.xml
index b26a55e0c..d25b975aa 100644
--- a/src/samples/alwaysencrypted/pom.xml
+++ b/src/samples/alwaysencrypted/pom.xml
@@ -1,4 +1,4 @@
-
4.0.0
@@ -15,7 +15,7 @@
com.microsoft.sqlserver
mssql-jdbc
- 12.6.0.jre11
+ 12.8.0.jre11
@@ -42,8 +42,8 @@
org.apache.maven.plugins
maven-compiler-plugin
- 18
- 18
+ 22
+ 22
@@ -53,4 +53,4 @@
-
+ -->
diff --git a/src/samples/azureactivedirectoryauthentication/pom.xml b/src/samples/azureactivedirectoryauthentication/pom.xml
index d704cd96f..8c1acabb3 100644
--- a/src/samples/azureactivedirectoryauthentication/pom.xml
+++ b/src/samples/azureactivedirectoryauthentication/pom.xml
@@ -1,4 +1,4 @@
-
4.0.0
com.microsoft.sqlserver.jdbc
@@ -14,7 +14,7 @@
com.microsoft.sqlserver
mssql-jdbc
- 12.6.0.jre11
+ 12.8.0.jre11
@@ -57,8 +57,8 @@
org.apache.maven.plugins
maven-compiler-plugin
- 18
- 18
+ 22
+ 22
@@ -68,4 +68,4 @@
-
+ -->
diff --git a/src/samples/connections/pom.xml b/src/samples/connections/pom.xml
index 7bc8fd947..bbc06a442 100644
--- a/src/samples/connections/pom.xml
+++ b/src/samples/connections/pom.xml
@@ -1,4 +1,4 @@
-
4.0.0
com.microsoft.sqlserver.jdbc
@@ -14,7 +14,7 @@
com.microsoft.sqlserver
mssql-jdbc
- 12.6.0.jre11
+ 12.8.0.jre11
@@ -57,8 +57,8 @@
org.apache.maven.plugins
maven-compiler-plugin
- 18
- 18
+ 22
+ 22
@@ -68,4 +68,4 @@
-
+ -->
diff --git a/src/samples/constrained/pom.xml b/src/samples/constrained/pom.xml
index 9518b2c53..330ab1830 100644
--- a/src/samples/constrained/pom.xml
+++ b/src/samples/constrained/pom.xml
@@ -1,4 +1,4 @@
-
+
diff --git a/src/samples/dataclassification/pom.xml b/src/samples/dataclassification/pom.xml
index f4f9f7ce4..c7743021c 100644
--- a/src/samples/dataclassification/pom.xml
+++ b/src/samples/dataclassification/pom.xml
@@ -1,4 +1,4 @@
-
+
diff --git a/src/samples/datatypes/pom.xml b/src/samples/datatypes/pom.xml
index 264dbb745..9052880a9 100644
--- a/src/samples/datatypes/pom.xml
+++ b/src/samples/datatypes/pom.xml
@@ -1,4 +1,4 @@
-
4.0.0
@@ -15,7 +15,7 @@
com.microsoft.sqlserver
mssql-jdbc
- 12.6.0.jre11
+ 12.8.0.jre11
@@ -74,8 +74,8 @@
org.apache.maven.plugins
maven-compiler-plugin
- 18
- 18
+ 22
+ 22
@@ -85,4 +85,4 @@
-
+ -->
diff --git a/src/samples/resultsets/pom.xml b/src/samples/resultsets/pom.xml
index 979b36010..c54636cd4 100644
--- a/src/samples/resultsets/pom.xml
+++ b/src/samples/resultsets/pom.xml
@@ -1,4 +1,4 @@
-
4.0.0
com.microsoft.sqlserver.jdbc
@@ -14,7 +14,7 @@
com.microsoft.sqlserver
mssql-jdbc
- 12.6.0.jre11
+ 12.8.0.jre11
@@ -73,8 +73,8 @@
org.apache.maven.plugins
maven-compiler-plugin
- 18
- 18
+ 22
+ 22
@@ -84,4 +84,4 @@
-
+ -->
diff --git a/src/samples/sparse/pom.xml b/src/samples/sparse/pom.xml
index 9df693086..3e7b15c94 100644
--- a/src/samples/sparse/pom.xml
+++ b/src/samples/sparse/pom.xml
@@ -1,4 +1,4 @@
-
4.0.0
com.microsoft.sqlserver.jdbc
@@ -14,7 +14,7 @@
com.microsoft.sqlserver
mssql-jdbc
- 12.6.0.jre11
+ 12.8.0.jre11
@@ -41,8 +41,8 @@
org.apache.maven.plugins
maven-compiler-plugin
- 18
- 18
+ 22
+ 22
@@ -52,4 +52,4 @@
-
+ -->
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/EnclavePackageTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/EnclavePackageTest.java
index d2ed9d088..5f3c1165c 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/EnclavePackageTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/EnclavePackageTest.java
@@ -230,7 +230,7 @@ public static void testBasicConnection(String cString, String protocol) throws E
AbstractTest.updateDataSource(cString, dsPool);
try (Connection con1 = dsLocal.getConnection(); Connection con2 = dsXA.getConnection();
- Connection con3 = dsPool.getConnection(); Connection con4 = PrepUtil.getConnection(cString)) {
+ Connection con3 = dsPool.getConnection()) {
if (TestUtils.isAEv2(con1)) {
verifyEnclaveEnabled(con1, protocol);
}
@@ -240,9 +240,6 @@ public static void testBasicConnection(String cString, String protocol) throws E
if (TestUtils.isAEv2(con3)) {
verifyEnclaveEnabled(con3, protocol);
}
- if (TestUtils.isAEv2(con4)) {
- verifyEnclaveEnabled(con4, protocol);
- }
}
}
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java
index b98171c23..ae34ef0c0 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java
@@ -42,6 +42,7 @@ protected Object[][] getContents() {
{"R_givenValueType", "The given value of type"},
{"R_lengthTruncated", " The inserted length is truncated or not correct!"},
{"R_timeValueTruncated", " The time value is truncated or not correct!"},
+ {"R_nullPointerExceptionFromResultSet", "Cannot invoke \"java.sql.ResultSet.next()\" because \"rs\" is null"},
{"R_invalidErrorMessage", "Invalid Error Message: "},
{"R_kerberosNativeGSSFailure",
"No valid credentials provided (Mechanism level: Failed to find any Kerberos tgt)"},
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java
index 928a59490..060c7648d 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java
@@ -1143,4 +1143,14 @@ public static void freeProcCache(Statement stmt) {
// ignore error - some tests fails due to permission issues from managed identity, this does not seem to affect tests
}
}
+
+ public static int getJVMVersion() {
+ String version = System.getProperty("java.version");
+ if(version.startsWith("1.")) {
+ version = version.substring(2, 3);
+ } else {
+ int dot = version.indexOf(".");
+ if(dot != -1) { version = version.substring(0, dot); }
+ } return Integer.parseInt(version);
+ }
}
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 a399e53f7..3a0daf194 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java
@@ -55,6 +55,8 @@ public class CallableStatementTest extends AbstractTest {
.escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_setNull_SP"));
private static String inputParamsProcedureName = AbstractSQLGenerator
.escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_inputParams_SP"));
+ private static String conditionalSproc = AbstractSQLGenerator
+ .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_conditionalSproc"));
private static String getObjectLocalDateTimeProcedureName = AbstractSQLGenerator
.escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_getObjectLocalDateTime_SP"));
private static String getObjectOffsetDateTimeProcedureName = AbstractSQLGenerator
@@ -65,6 +67,8 @@ public class CallableStatementTest extends AbstractTest {
.escapeIdentifier(RandomUtil.getIdentifier("manyParam_Table"));
private static String manyParamProc = AbstractSQLGenerator
.escapeIdentifier(RandomUtil.getIdentifier("manyParam_Procedure"));
+ private static String currentTimeProc = AbstractSQLGenerator
+ .escapeIdentifier(RandomUtil.getIdentifier("currentTime_Procedure"));
private static String manyParamUserDefinedType = AbstractSQLGenerator
.escapeIdentifier(RandomUtil.getIdentifier("manyParam_definedType"));
private static String zeroParamSproc = AbstractSQLGenerator
@@ -95,6 +99,7 @@ public static void setupTest() throws Exception {
TestUtils.dropProcedureIfExists(zeroParamSproc, stmt);
TestUtils.dropProcedureIfExists(outOfOrderSproc, stmt);
TestUtils.dropProcedureIfExists(byParamNameSproc, stmt);
+ TestUtils.dropProcedureIfExists(conditionalSproc, stmt);
TestUtils.dropFunctionIfExists(userDefinedFunction, stmt);
TestUtils.dropUserDefinedTypeIfExists(manyParamUserDefinedType, stmt);
TestUtils.dropProcedureIfExists(manyParamProc, stmt);
@@ -108,10 +113,12 @@ public static void setupTest() throws Exception {
createUserDefinedType();
createTableManyParams();
createProcedureManyParams();
+ createProcedureCurrentTime();
createGetObjectOffsetDateTimeProcedure(stmt);
createProcedureZeroParams();
createOutOfOrderSproc();
createByParamNameSproc();
+ createConditionalProcedure();
createUserDefinedFunction();
}
}
@@ -379,6 +386,17 @@ public void testZeroParamSproc() throws SQLException {
cs.execute();
assertEquals(1, cs.getInt(1));
}
+
+ // Test zero parameter sproc with return value with parentheses
+ call = "{? = CALL " + zeroParamSproc + "()}";
+
+ try (CallableStatement cs = connection.prepareCall(call)) {
+ cs.registerOutParameter(1, Types.INTEGER);
+ cs.execute();
+ // Calling zero parameter sproc with return value with parentheses
+ // should return a value that's not zero
+ assertEquals(1, cs.getInt(1));
+ }
}
@Test
@@ -1147,6 +1165,38 @@ public void testExecDocumentedSystemStoredProceduresIndexedParameters() throws S
}
}
+ @Test
+ public void testCallableStatementDefaultValues() throws SQLException {
+ String call0 = "{call " + conditionalSproc + " (?, ?, 1)}";
+ String call1 = "{call " + conditionalSproc + " (?, ?, 2)}";
+ int expectedValue = 5; // The sproc should return this value
+
+ try (CallableStatement cstmt = connection.prepareCall(call0)) {
+ cstmt.setInt(1, 1);
+ cstmt.setInt(2, 2);
+ cstmt.execute();
+ ResultSet rs = cstmt.getResultSet();
+ rs.next();
+ fail(TestResource.getResource("R_expectedFailPassed"));
+
+ } catch (Exception e) {
+ String msg = e.getMessage();
+ assertTrue(TestResource
+ .getResource("R_nullPointerExceptionFromResultSet").equalsIgnoreCase(msg)
+ || msg == null);
+ }
+
+ try (CallableStatement cstmt = connection.prepareCall(call1)) {
+ cstmt.setInt(1, 1);
+ cstmt.setInt(2, 2);
+ cstmt.execute();
+ ResultSet rs = cstmt.getResultSet();
+ rs.next();
+
+ assertEquals(Integer.toString(expectedValue), rs.getString(1));
+ }
+ }
+
@Test
@Tag(Constants.reqExternalSetup)
@Tag(Constants.xAzureSQLDB)
@@ -1224,6 +1274,17 @@ public void testFourPartSyntaxCallEscapeSyntax() throws SQLException {
}
}
+ @Test
+ public void testTimestampStringConversion() throws SQLException {
+ try (CallableStatement stmt = connection.prepareCall("{call " + currentTimeProc + "(?)}")) {
+ String timestamp = "2024-05-29 15:35:53.461";
+ stmt.setObject(1, timestamp, Types.TIMESTAMP);
+ stmt.registerOutParameter(1, Types.TIMESTAMP);
+ stmt.execute();
+ stmt.getObject("currentTimeStamp");
+ }
+ }
+
/**
* Cleanup after test
*
@@ -1242,6 +1303,8 @@ public static void cleanup() throws SQLException {
TestUtils.dropProcedureIfExists(zeroParamSproc, stmt);
TestUtils.dropProcedureIfExists(outOfOrderSproc, stmt);
TestUtils.dropProcedureIfExists(byParamNameSproc, stmt);
+ TestUtils.dropProcedureIfExists(currentTimeProc, stmt);
+ TestUtils.dropProcedureIfExists(conditionalSproc, stmt);
TestUtils.dropFunctionIfExists(userDefinedFunction, stmt);
}
}
@@ -1294,6 +1357,22 @@ private static void createProcedureManyParams() throws SQLException {
}
}
+ private static void createProcedureCurrentTime() throws SQLException {
+ String sql = "CREATE PROCEDURE " + currentTimeProc + " @currentTimeStamp datetime = null OUTPUT " +
+ "AS BEGIN SET @currentTimeStamp = CURRENT_TIMESTAMP; END";
+ try (Statement stmt = connection.createStatement()) {
+ stmt.execute(sql);
+ }
+ }
+
+ private static void createConditionalProcedure() throws SQLException {
+ String sql = "CREATE PROCEDURE " + conditionalSproc + " @param0 INT, @param1 INT, @maybe bigint = 2 " +
+ "AS BEGIN IF @maybe >= 2 BEGIN SELECT 5 END END";
+ try (Statement stmt = connection.createStatement()) {
+ stmt.execute(sql);
+ }
+ }
+
private static void createTableManyParams() throws SQLException {
String type = manyParamUserDefinedType;
String sql = "CREATE TABLE" + manyParamsTable + " (c1 " + type + " null, " + "c2 " + type + " null, " + "c3 "
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 9c1a7b2d9..3fdd65be8 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java
@@ -516,6 +516,8 @@ private List getVerifiedMethodNames() {
verifiedMethodNames.add("getServerMessageHandler");
verifiedMethodNames.add("setServerMessageHandler");
verifiedMethodNames.add("getCalcBigDecimalScale");
+ verifiedMethodNames.add("setcacheBulkCopyMetadata");
+ verifiedMethodNames.add("getcacheBulkCopyMetadata");
verifiedMethodNames.add("setCalcBigDecimalScale");
verifiedMethodNames.add("getUseFlexibleCallableStatements");
verifiedMethodNames.add("setUseFlexibleCallableStatements");
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 47b771dc3..00e99a0f7 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
@@ -4,6 +4,8 @@
*/
package com.microsoft.sqlserver.jdbc.unit.statement;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assume.assumeTrue;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -23,6 +25,7 @@
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.Calendar;
+import java.util.HashMap;
import java.util.TimeZone;
import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement;
@@ -109,6 +112,100 @@ public void testBatchUpdateCountTrueOnFirstPstmtSpPrepare() throws Exception {
testBatchUpdateCountWith(5, 4, true, "prepare", expectedUpdateCount);
}
+ @Test
+ public void testSqlServerBulkCopyCachingPstmtLevel() throws Exception {
+ Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
+ long ms = 1578743412000L;
+
+ try (Connection con = DriverManager.getConnection(
+ connectionString + ";useBulkCopyForBatchInsert=true;sendTemporalDataTypesAsStringForBulkCopy=false;");
+ 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);
+
+ Field cachedBulkCopyOperationField = pstmt.getClass().getDeclaredField("bcOperation");
+ cachedBulkCopyOperationField.setAccessible(true);
+ Object cachedBulkCopyOperation = cachedBulkCopyOperationField.get(pstmt);
+ assertEquals(null, cachedBulkCopyOperation, "SqlServerBulkCopy object should not be cached yet.");
+
+ Timestamp timestamp = new Timestamp(ms);
+
+ pstmt.setTimestamp(1, timestamp, gmtCal);
+ pstmt.addBatch();
+ pstmt.executeBatch();
+
+ cachedBulkCopyOperation = cachedBulkCopyOperationField.get(pstmt);
+ assertNotNull("SqlServerBulkCopy object should be cached.", cachedBulkCopyOperation);
+ }
+ }
+
+ @Test
+ public void testSqlServerBulkCopyCachingConnectionLevel() throws Exception {
+ Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
+ long ms = 1578743412000L;
+
+ try (SQLServerConnection con = (SQLServerConnection) DriverManager.getConnection(
+ connectionString + ";useBulkCopyForBatchInsert=true;cacheBulkCopyMetadata=true;sendTemporalDataTypesAsStringForBulkCopy=false;");
+ Statement stmt = con.createStatement()) {
+
+ // Needs to be on a JDK version greater than 8
+ assumeTrue(TestUtils.getJVMVersion() > 8);
+
+ TestUtils.dropTableIfExists(timestampTable1, stmt);
+ TestUtils.dropTableIfExists(timestampTable2, stmt);
+ String createSqlTable1 = "CREATE TABLE " + timestampTable1 + " (c1 DATETIME2(3))";
+ String createSqlTable2 = "CREATE TABLE " + timestampTable2 + " (c1 DATETIME2(3))";
+ stmt.execute(createSqlTable1);
+ stmt.execute(createSqlTable2);
+
+ Field bulkcopyMetadataCacheField;
+
+ if (con.getClass().getName().equals("com.microsoft.sqlserver.jdbc.SQLServerConnection43")) {
+ bulkcopyMetadataCacheField = con.getClass().getSuperclass()
+ .getDeclaredField("BULK_COPY_OPERATION_CACHE");
+ } else {
+ bulkcopyMetadataCacheField = con.getClass().getDeclaredField("BULK_COPY_OPERATION_CACHE");
+ }
+
+ bulkcopyMetadataCacheField.setAccessible(true);
+ Object bulkcopyCache = bulkcopyMetadataCacheField.get(con);
+
+ assertTrue(((HashMap) bulkcopyCache).isEmpty(), "Cache should be empty");
+
+ for (int i = 0; i < 5; i++) {
+ PreparedStatement pstmt = con.prepareStatement("INSERT INTO " + timestampTable1 + " VALUES(?)");
+ Timestamp timestamp = new Timestamp(ms);
+ pstmt.setTimestamp(1, timestamp, gmtCal);
+ pstmt.addBatch();
+ pstmt.executeBatch();
+
+ bulkcopyCache = bulkcopyMetadataCacheField.get(con);
+ assertTrue(!((HashMap) bulkcopyCache).isEmpty(), "Cache should not be empty");
+ }
+
+ // Cache should have 1 metadata item cached
+ assertEquals(1, ((HashMap) bulkcopyCache).size());
+
+ // Execute a different batch call on a different table
+ for (int i = 0; i < 5; i++) {
+ PreparedStatement pstmt = con.prepareStatement("INSERT INTO " + timestampTable2 + " VALUES(?)");
+ Timestamp timestamp = new Timestamp(ms);
+ pstmt.setTimestamp(1, timestamp, gmtCal);
+ pstmt.addBatch();
+ pstmt.executeBatch();
+
+ bulkcopyCache = bulkcopyMetadataCacheField.get(con);
+ assertTrue(!((HashMap) bulkcopyCache).isEmpty(), "Cache should not be empty");
+ }
+
+ // Cache should now have 2 metadata items cached
+ assertEquals(2, ((HashMap) bulkcopyCache).size());
+ }
+ }
+
@Test
public void testValidTimezoneForTimestampBatchInsertWithBulkCopy() throws Exception {
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));