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}}"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9eedd3bef..945c12e34 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,26 +3,66 @@ 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)
+- 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
+- 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)
@@ -89,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)
@@ -144,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
@@ -207,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)
@@ -736,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)
diff --git a/README.md b/README.md
index ffbc04353..4f3f01687 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
@@ -79,10 +81,10 @@ We're now on the Maven Central Repository. Add the following to your POM file to
com.microsoft.sqlservermssql-jdbc
- 12.4.1.jre11
+ 12.6.0.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:
@@ -90,7 +92,7 @@ To get the latest version of the driver, add the following to your POM file:
com.microsoft.sqlservermssql-jdbc
- 12.4.1.jre11
+ 12.6.0.jre11
```
@@ -104,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:
@@ -125,7 +127,7 @@ Projects that require either of the two features need to explicitly declare the
com.microsoft.sqlservermssql-jdbc
- 12.4.1.jre11
+ 12.6.0.jre11compile
@@ -143,7 +145,7 @@ Projects that require either of the two features need to explicitly declare the
com.microsoft.sqlservermssql-jdbc
- 12.4.1.jre11
+ 12.6.0.jre11compile
@@ -170,7 +172,7 @@ When setting 'useFmtOnly' property to 'true' for establishing a connection or cr
com.microsoft.sqlservermssql-jdbc
- 12.4.1.jre11
+ 12.6.0.jre11
@@ -210,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..f862b13d4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -11,7 +11,7 @@
apply plugin: 'java'
-version = '12.5.0'
+version = '12.7.1-SNAPSHOT'
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 e5f8973c2..7b1555e09 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.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 a87128a09..7a123f3cf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
4.0.0com.microsoft.sqlservermssql-jdbc
- 12.5.0
+ 12.7.1-SNAPSHOTjarMicrosoft JDBC Driver for SQL Server
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/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/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
index 14c7b0d4a..4673a9e7c 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;
@@ -672,7 +673,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 +774,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 +840,7 @@ void disableSSL() {
channelSocket = tcpSocket;
sslSocket = null;
} finally {
- lock.unlock();
+ tdsChannelLock.unlock();
}
if (logger.isLoggable(Level.FINER))
@@ -1056,6 +1057,8 @@ private void writeInternal(byte[] b, int off, int len) throws IOException {
private final class ProxyInputStream extends InputStream {
private InputStream filteredStream;
+ private final Lock proxyInputStreamLock = new ReentrantLock();
+
/**
* Bytes that have been read by a poll(s).
*/
@@ -1082,7 +1085,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 +1120,7 @@ public boolean poll() {
return true;
} finally {
- lock.unlock();
+ proxyInputStreamLock.unlock();
}
}
@@ -1133,7 +1136,7 @@ private int getOneFromCache() {
@Override
public long skip(long n) throws IOException {
- lock.lock();
+ proxyInputStreamLock.lock();
try {
long bytesSkipped = 0;
@@ -1154,7 +1157,7 @@ public long skip(long n) throws IOException {
return bytesSkipped;
} finally {
- lock.unlock();
+ proxyInputStreamLock.unlock();
}
}
@@ -1191,7 +1194,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 +1243,7 @@ private int readInternal(byte[] b, int offset, int maxBytes) throws IOException
return bytesRead;
} finally {
- lock.unlock();
+ proxyInputStreamLock.unlock();
}
}
@@ -1259,11 +1262,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 +1275,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();
}
}
@@ -1438,7 +1441,7 @@ public int getPort() {
}
@Override
- public synchronized int getReceiveBufferSize() throws SocketException {
+ public int getReceiveBufferSize() throws SocketException {
return tdsChannel.tcpSocket.getReceiveBufferSize();
}
@@ -1453,7 +1456,7 @@ public boolean getReuseAddress() throws SocketException {
}
@Override
- public synchronized int getSendBufferSize() throws SocketException {
+ public int getSendBufferSize() throws SocketException {
return tdsChannel.tcpSocket.getSendBufferSize();
}
@@ -1463,7 +1466,7 @@ public int getSoLinger() throws SocketException {
}
@Override
- public synchronized int getSoTimeout() throws SocketException {
+ public int getSoTimeout() throws SocketException {
return tdsChannel.tcpSocket.getSoTimeout();
}
@@ -1534,19 +1537,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);
}
@@ -1564,7 +1567,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);
}
@@ -2364,6 +2367,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());
+ }
}
@@ -3823,6 +3830,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)
@@ -3836,6 +3857,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
@@ -4754,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.
*
@@ -6688,7 +6749,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 +6869,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;
@@ -6882,6 +6943,11 @@ final boolean readPacket() throws SQLServerException {
// if messageType is RPC or QUERY, then increment Counter's state
if (tdsChannel.getWriter().checkIfTdsMessageTypeIsBatchOrRPC() && null != command) {
+ 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);
}
@@ -6921,7 +6987,7 @@ final boolean readPacket() throws SQLServerException {
return true;
} finally {
- lock.unlock();
+ tdsReaderLock.unlock();
}
}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
index 7ab7772d7..2479e5063 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
@@ -479,9 +479,29 @@ 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);
+ /**
+ * Get Currently installed message handler on the connection
+ *
+ * @see ISQLServerMessageHandler#messageHandler(ISQLServerMessage)
+ * @return ISQLServerMessageHandler
+ */
+ ISQLServerMessageHandler getServerMessageHandler();
+
+ /**
+ * Set message handler on the connection
+ *
+ * @param messageHandler
+ * message handler
+ *
+ * @see ISQLServerMessageHandler#messageHandler(ISQLServerMessage)
+ * @return ISQLServerMessageHandler
+ */
+ ISQLServerMessageHandler setServerMessageHandler(ISQLServerMessageHandler messageHandler);
+
/**
* Returns the current flag for calcBigDecimalPrecision.
*
@@ -498,14 +518,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/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/ISQLServerMessage.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessage.java
new file mode 100644
index 000000000..622b865c3
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessage.java
@@ -0,0 +1,99 @@
+/*
+ * 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;
+
+
+/**
+ * Provides an interface SQLServerMessage
+ */
+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..2f0b71de7
--- /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.
+ *
+ *
+ * @param serverErrorOrWarning
+ * 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/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/PersistentTokenCacheAccessAspect.java b/src/main/java/com/microsoft/sqlserver/jdbc/PersistentTokenCacheAccessAspect.java
index 31b87fb27..24458a7f3 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,25 @@ 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;
+ }
+
/**
* 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/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/SQLJdbcVersion.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java
index 8adbb06fc..8a975e539 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java
@@ -7,8 +7,8 @@
final class SQLJdbcVersion {
static final int MAJOR = 12;
- static final int MINOR = 5;
- static final int PATCH = 0;
+ static final int MINOR = 7;
+ static final int PATCH = 1;
static final int BUILD = 0;
/*
* Used to load mssql-jdbc_auth DLL.
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/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/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java
index ec711082d..8f20b2b8b 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java
@@ -2065,7 +2065,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) throws SQLServerException {
+ int srcColOrdinal, int destColOrdinal, boolean isStreaming, Object colValue,
+ Calendar cal) throws SQLServerException {
SSType destSSType = destColumnMetadata.get(destColOrdinal).ssType;
bulkPrecision = validateSourcePrecision(bulkPrecision, bulkJdbcType,
@@ -2481,10 +2482,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,8 +2989,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) 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;
@@ -3103,7 +3111,7 @@ else if (null != serverBulkData && (null == destCryptoMeta)) {
}
}
writeColumnToTdsWriter(tdsWriter, srcPrecision, srcScale, srcJdbcType, srcNullable, srcColOrdinal,
- destColOrdinal, isStreaming, colValue);
+ destColOrdinal, isStreaming, colValue, cal);
}
/**
@@ -3635,7 +3643,7 @@ private boolean writeBatchData(TDSWriter tdsWriter, TDSCommand command,
// where multiple source columns can be mapped to one destination column.
for (ColumnMapping columnMapping : columnMappings) {
writeColumn(tdsWriter, columnMapping.sourceColumnOrdinal, columnMapping.destinationColumnOrdinal,
- null // cell
+ null, null // cell
// value is
// retrieved
// inside
@@ -3648,20 +3656,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/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 319b2b268..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)
@@ -391,21 +407,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 a17d0b0b4..5edf4e0c7 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[]
*/
@@ -1050,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();
@@ -1753,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;
@@ -1791,6 +1810,11 @@ final Connection getConnection() {
final void resetPooledConnection() {
tdsChannel.resetPooledConnection();
initResettableValues();
+
+ // reset prepared statement handle cache
+ if (null != preparedStatementHandleCache) {
+ preparedStatementHandleCache.clear();
+ }
}
/**
@@ -2137,9 +2161,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;
}
}
}
@@ -3204,9 +3228,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)
@@ -3219,6 +3249,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
@@ -3234,6 +3279,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;
@@ -3431,34 +3477,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
@@ -3466,15 +3502,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
@@ -3493,15 +3521,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;
@@ -3514,19 +3534,22 @@ 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)
@@ -3619,31 +3642,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,
@@ -3704,7 +3718,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
@@ -4344,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.");
@@ -4795,6 +4811,8 @@ private void clearConnectionResources() {
// Clean-up queue etc. related to batching of prepared statement discard actions (sp_unprepare).
cleanupPreparedStatementDiscardActions();
+
+ ActivityCorrelator.cleanupActivityId();
}
/**
@@ -4816,6 +4834,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");
}
@@ -4940,6 +4960,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();
@@ -5976,7 +6017,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())) {
@@ -6075,7 +6116,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());
@@ -6099,13 +6140,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,
@@ -8057,6 +8099,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;
@@ -8382,7 +8425,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;
@@ -8508,6 +8551,31 @@ 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
+ * message handler
+ * @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 4c40b01c6..899f10595 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'.
*
@@ -712,6 +722,7 @@ public boolean getCalcBigDecimalPrecision() {
public void setCalcBigDecimalPrecision(boolean calcBigDecimalPrecision) {
wrappedConnection.setCalcBigDecimalPrecision(calcBigDecimalPrecision);
}
+
/**
* Returns the useBulkCopyForBatchInsert value.
*
@@ -719,7 +730,7 @@ public void setCalcBigDecimalPrecision(boolean calcBigDecimalPrecision) {
*/
@Override
public boolean getUseBulkCopyForBatchInsert() {
- return wrappedConnection.getUseBulkCopyForBatchInsert();
+ return wrappedConnection.getUseBulkCopyForBatchInsert();
}
/**
@@ -730,6 +741,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/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) {
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java
index 1d2868879..12cc843b7 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java
@@ -258,11 +258,20 @@ 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;
+
+ /** getTypes columns */
+ private LinkedHashMap getTypesDWColumns = null;
+
/** getImportedKeys columns */
private volatile LinkedHashMap getImportedKeysDWColumns;
private static final Lock LOCK = new ReentrantLock();
@@ -630,10 +639,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 +733,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 +803,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 +839,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();
@@ -1350,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);
@@ -1394,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/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/SQLServerError.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerError.java
index 3454e5aef..b3e96904f 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,103 @@ 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 ISQLServerMessage
+ */
+ 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 ISQLServerMessage
+ */
+ 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 ISQLServerMessage
+ */
+ 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
+ * new severity
+ */
+ public void setErrorSeverity(int newSeverity) {
+ this.errorSeverity = newSeverity;
+ }
+
+ /**
+ * Set a new ErrorNumber for this Message
+ *
+ * @param newErrorNumber
+ * new error number
+ */
+ 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 7d4fa23f0..57387bb03 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,14 @@ 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 +270,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);
@@ -395,16 +419,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/main/java/com/microsoft/sqlserver/jdbc/SQLServerInfoMessage.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerInfoMessage.java
new file mode 100644
index 000000000..0b3a6075a
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerInfoMessage.java
@@ -0,0 +1,135 @@
+/*
+ * 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 ISQLServerMessage
+ */
+ 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 ISQLServerMessage
+ */
+ 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 ISQLServerMessage
+ */
+ 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/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/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java
index 921212391..ddf88ed2b 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,15 @@ 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. */
final int[] userSQLParamPositions;
@@ -128,6 +138,22 @@ 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");
+
+ /**
+ * Regex for four part syntax
+ */
+ private static final Pattern fourPartSyntaxPattern = Pattern.compile("(.+)\\.(.+)\\.(.+)\\.(.+)");
+
/** Returns the prepared statement SQL */
@Override
public String toString() {
@@ -208,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.
*
@@ -228,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"};
@@ -253,6 +288,9 @@ private boolean resetPrepStmtHandle(boolean discardCurrentCacheItem) {
procedureName = parsedSQL.procedureName;
bReturnValueSyntax = parsedSQL.bReturnValueSyntax;
userSQL = parsedSQL.processedSQL;
+ isExecEscapeSyntax = isExecEscapeSyntax(sql);
+ isCallEscapeSyntax = isCallEscapeSyntax(sql);
+ isFourPartSyntax = isFourPartSyntax(sql);
userSQLParamPositions = parsedSQL.parameterPositions;
initParams(userSQLParamPositions.length);
useBulkCopyForBatchInsert = conn.getUseBulkCopyForBatchInsert();
@@ -262,6 +300,8 @@ private boolean resetPrepStmtHandle(boolean discardCurrentCacheItem) {
* Closes the prepared statement's prepared handle.
*/
private void closePreparedHandle() {
+ connection.removeBeforeReconnectListener(clearPrepStmtHandleOnReconnectListener);
+
if (!hasPreparedStatementHandle())
return;
@@ -487,6 +527,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 +542,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 +555,8 @@ public int executeUpdate() throws SQLServerException, SQLTimeoutException {
}
checkClosed();
- connection.unprepareUnreferencedPreparedStatementHandles(true);
+
+ connection.unprepareUnreferencedPreparedStatementHandles(false);
executeStatement(new PrepStmtExecCmd(this, EXECUTE_UPDATE));
// this shouldn't happen, caller probably meant to call executeLargeUpdate
@@ -534,7 +577,7 @@ public long executeLargeUpdate() throws SQLServerException, SQLTimeoutException
loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString());
}
checkClosed();
- connection.unprepareUnreferencedPreparedStatementHandles(true);
+ connection.unprepareUnreferencedPreparedStatementHandles(false);
executeStatement(new PrepStmtExecCmd(this, EXECUTE_UPDATE));
loggerExternal.exiting(getClassNameLogging(), "executeLargeUpdate", updateCount);
return updateCount;
@@ -547,7 +590,7 @@ public boolean execute() throws SQLServerException, SQLTimeoutException {
loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString());
}
checkClosed();
- connection.unprepareUnreferencedPreparedStatementHandles(true);
+ connection.unprepareUnreferencedPreparedStatementHandles(false);
executeStatement(new PrepStmtExecCmd(this, EXECUTE));
loggerExternal.exiting(getClassNameLogging(), "execute", null != resultSet);
return null != resultSet;
@@ -1208,7 +1251,18 @@ 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 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 && !isFourPartSyntax);
}
/**
@@ -1228,6 +1282,18 @@ 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();
+ }
+
+ private boolean isFourPartSyntax(String sql) {
+ return fourPartSyntaxPattern.matcher(sql).find();
+ }
+
/**
* Executes sp_prepare to prepare a parameterized statement and sets the prepared statement handle
*
@@ -2201,7 +2267,7 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL
loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString());
}
checkClosed();
- connection.unprepareUnreferencedPreparedStatementHandles(true);
+ connection.unprepareUnreferencedPreparedStatementHandles(false);
discardLastExecutionResults();
try {
@@ -2385,6 +2451,7 @@ public long[] executeLargeBatch() throws SQLServerException, BatchUpdateExceptio
loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString());
}
checkClosed();
+ connection.unprepareUnreferencedPreparedStatementHandles(false);
discardLastExecutionResults();
try {
@@ -3419,6 +3486,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");
}
@@ -3430,6 +3498,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");
}
@@ -3529,4 +3598,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/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
index 95b5ffa0b..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}."},
@@ -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/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java
index 4e096864d..12432c560 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;
@@ -1616,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())
@@ -1686,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.
@@ -1701,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..de6df2a42
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerWarning.java
@@ -0,0 +1,45 @@
+/*
+ * 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/SQLServerXAResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java
index 6d82bf836..5c451b22b 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,8 +24,6 @@
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
-import com.microsoft.sqlserver.jdbc.SQLServerError.TransientError;
-
/**
* Implements Transaction id used to recover transactions.
@@ -754,27 +751,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 +928,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/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/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);
- }
}
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/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/main/java/microsoft/sql/DateTimeOffset.java b/src/main/java/microsoft/sql/DateTimeOffset.java
index e2fcc8409..bf9e95c7b 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
+ * 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
+ */
+ 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
+ * 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
+ */
+ public static DateTimeOffset valueOf(java.time.OffsetDateTime offsetDateTime) {
+ return new DateTimeOffset(offsetDateTime);
+ }
+
/** formatted value */
private String formattedValue = null;
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.sqlservermssql-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.sqlservermssql-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.sqlservermssql-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.sqlservermssql-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.sqlservermssql-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.sqlservermssql-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.sqlservermssql-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.sqlservermssql-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.sqlservermssql-jdbc
- 12.4.0.jre11
+ 12.6.0.jre11
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java
index 04fedc1ff..12eea46cf 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");
@@ -209,7 +210,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"));
@@ -488,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)
@@ -980,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/TestResource.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java
index e14bb671a..b3e854fd1 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java
@@ -63,6 +63,10 @@ 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"},
@@ -212,5 +216,7 @@ 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/TestUtils.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java
index 2f20821a1..4283a80f5 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.
*
@@ -410,6 +419,18 @@ 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/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