diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java index 62fdeda8b0..c00884cc50 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java @@ -1451,8 +1451,8 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource { * @return cacheBulkCopyMetadata boolean value */ boolean getcacheBulkCopyMetadata(); - - /** + + /** * Returns value of 'retryExec' from Connection String. * * @param retryExec @@ -1506,4 +1506,38 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource { * @return useFlexibleCallableStatements */ boolean getUseFlexibleCallableStatements(); + + /** + * Returns value of 'quotedIdentifier' from Connection String. + * + * @return true + * if quotedIdentifier is set to true, false otherwise + */ + String getQuotedIdentifier(); + + /** + * Sets the value for 'quotedIdentifier' property + * + * @param quotedIdentifier + * boolean value + * + */ + void setQuotedIdentifier(String quotedIdentifier); + + /** + * Returns value of 'concatNullYieldsNull' from Connection String. + * + * @return true + * if concatNullYieldsNull is set to true, false otherwise + */ + String getConcatNullYieldsNull(); + + /** + * Sets the value for 'concatNullYieldsNull' property + * + * @param concatNullYieldsNull + * boolean value + * + */ + void setConcatNullYieldsNull(String concatNullYieldsNull); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index b64e9459c0..4919a07816 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -2038,7 +2038,7 @@ final Connection getConnection() { return this; } - final void resetPooledConnection() { + final void resetPooledConnection() throws SQLServerException { tdsChannel.resetPooledConnection(); initResettableValues(); @@ -2051,6 +2051,8 @@ final void resetPooledConnection() { if (null != bulkCopyOperationCache) { bulkCopyOperationCache.clear(); } + + setSessionProperties(); } /** @@ -3530,7 +3532,8 @@ else if (0 == requestedPacketSize) } state = State.OPENED; - + setSessionProperties(); + // Socket timeout is bounded by loginTimeout during the login phase. // Reset socket timeout back to the original value. tdsChannel.resetTcpSocketTimeout(); @@ -3552,6 +3555,40 @@ else if (0 == requestedPacketSize) return this; } + private void setSessionProperties() throws SQLServerException { + // check QUOTED_IDENTIFIER property + String quotedIdentifierProperty = SQLServerDriverStringProperty.QUOTED_IDENTIFIER.toString(); + String quotedIdentifierValue = activeConnectionProperties.getProperty(quotedIdentifierProperty); + if (null != quotedIdentifierValue) { + OnOffOption quotedIdentifierOption = OnOffOption.valueOfString(quotedIdentifierValue); + activeConnectionProperties.setProperty(quotedIdentifierProperty, quotedIdentifierValue); + switch (quotedIdentifierOption) { + case ON: + connectionCommand("SET QUOTED_IDENTIFIER ON", "quotedIdentifier"); + break; + case OFF: + connectionCommand("SET QUOTED_IDENTIFIER OFF", "quotedIdentifier"); + break; + } + } + + // check CONCAT_NULL_YIELDS_NULL property + String concatNullYieldsNullProperty = SQLServerDriverStringProperty.CONCAT_NULL_YIELDS_NULL.toString(); + String concatNullYieldsNullValue = activeConnectionProperties.getProperty(concatNullYieldsNullProperty); + if (null != concatNullYieldsNullValue) { + OnOffOption concatNullYieldsOption = OnOffOption.valueOfString(concatNullYieldsNullValue); + activeConnectionProperties.setProperty(concatNullYieldsNullProperty, concatNullYieldsNullValue); + switch (concatNullYieldsOption) { + case ON: + connectionCommand("SET CONCAT_NULL_YIELDS_NULL ON", "concatNullYields"); + break; + case OFF: + connectionCommand("SET CONCAT_NULL_YIELDS_NULL OFF", "concatNullYields"); + break; + } + } + } + // log open connection failures private void logConnectFailure(int attemptNumber, SQLServerException e, SQLServerError sqlServerError) { loggerResiliency.finer(toString() + " Connection open - connection failed on attempt: " + attemptNumber + "."); @@ -4786,6 +4823,7 @@ boolean executeReconnectCommand(TDSCommand newCommand) throws SQLServerException */ boolean commandComplete = false; try { + newCommand.createCounter(null, activeConnectionProperties); commandComplete = newCommand.execute(tdsChannel.getWriter(), tdsChannel.getReader(newCommand)); } finally { /* diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java index 61b9f8f0cb..6267d652d1 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java @@ -948,17 +948,15 @@ public boolean getEnablePrepareOnFirstPreparedStatementCall() { @Override public void setcacheBulkCopyMetadata(boolean cacheBulkCopyMetadata) { - setBooleanProperty(connectionProps, - SQLServerDriverBooleanProperty.ENABLE_BULK_COPY_CACHE.toString(), + setBooleanProperty(connectionProps, SQLServerDriverBooleanProperty.ENABLE_BULK_COPY_CACHE.toString(), cacheBulkCopyMetadata); } @Override public boolean getcacheBulkCopyMetadata() { - boolean defaultValue = SQLServerDriverBooleanProperty.ENABLE_BULK_COPY_CACHE - .getDefaultValue(); - return getBooleanProperty(connectionProps, - SQLServerDriverBooleanProperty.ENABLE_BULK_COPY_CACHE.toString(), defaultValue); + boolean defaultValue = SQLServerDriverBooleanProperty.ENABLE_BULK_COPY_CACHE.getDefaultValue(); + return getBooleanProperty(connectionProps, SQLServerDriverBooleanProperty.ENABLE_BULK_COPY_CACHE.toString(), + defaultValue); } @Override @@ -1392,7 +1390,6 @@ public int getMsiTokenCacheTtl() { @Override public void setUseFlexibleCallableStatements(boolean enable) {} - /** * useFlexibleCallableStatements is temporarily removed. * This method is a no-op for backwards compatibility only. @@ -1494,6 +1491,29 @@ public String getRetryExec() { return getStringProperty(connectionProps, SQLServerDriverStringProperty.RETRY_EXEC.toString(), null); } + @Override + public String getQuotedIdentifier() { + return getStringProperty(connectionProps, SQLServerDriverStringProperty.QUOTED_IDENTIFIER.toString(), + SQLServerDriverStringProperty.QUOTED_IDENTIFIER.getDefaultValue()); + } + + @Override + public void setQuotedIdentifier(String quotedIdentifier) { + setStringProperty(connectionProps, SQLServerDriverStringProperty.QUOTED_IDENTIFIER.toString(), quotedIdentifier); + } + + @Override + public String getConcatNullYieldsNull() { + return getStringProperty(connectionProps, SQLServerDriverStringProperty.CONCAT_NULL_YIELDS_NULL.toString(), + SQLServerDriverStringProperty.CONCAT_NULL_YIELDS_NULL.getDefaultValue()); + } + + @Override + public void setConcatNullYieldsNull(String concatNullYieldNull) { + setStringProperty(connectionProps, SQLServerDriverStringProperty.CONCAT_NULL_YIELDS_NULL.toString(), + concatNullYieldNull); + } + /** * Sets the 'retryConn' setting. * diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java index a265d80990..aa1c0c5c0c 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java @@ -612,7 +612,9 @@ enum SQLServerDriverStringProperty { DATETIME_DATATYPE("datetimeParameterType", DatetimeType.DATETIME2.toString()), ACCESS_TOKEN_CALLBACK_CLASS("accessTokenCallbackClass", ""), RETRY_EXEC("retryExec", ""), - RETRY_CONN("retryConn", ""); + RETRY_CONN("retryConn", ""), + QUOTED_IDENTIFIER("quotedIdentifier", OnOffOption.ON.toString()), + CONCAT_NULL_YIELDS_NULL("concatNullYieldsNull", OnOffOption.ON.toString()); private final String name; private final String defaultValue; @@ -728,6 +730,47 @@ public String toString() { } +enum OnOffOption { + ON("ON"), + OFF("OFF"); + + private final String option; + + private OnOffOption(String option) { + this.option = option; + } + + @Override + public String toString() { + return option; + } + + static OnOffOption valueOfString(String value) throws SQLServerException { + OnOffOption option = null; + + if (value.toLowerCase(Locale.US).equalsIgnoreCase(OnOffOption.ON.toString())) { + option = OnOffOption.ON; + } else if (value.toLowerCase(Locale.US).equalsIgnoreCase(OnOffOption.OFF.toString())) { + option = OnOffOption.OFF; + } else { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidConnectionSetting")); + Object[] msgArgs = {"OnOffOption", value}; + throw new SQLServerException(form.format(msgArgs), null); + } + return option; + } + + static boolean isValidOnOffOption(String option) { + for (OnOffOption t : OnOffOption.values()) { + if (option.equalsIgnoreCase(t.toString())) { + return true; + } + } + return false; + } +} + + /** * Provides methods to connect to a SQL Server database and to obtain information about the JDBC driver. */ @@ -807,8 +850,8 @@ public final class SQLServerDriver implements java.sql.Driver { Boolean.toString(SQLServerDriverBooleanProperty.INTEGRATED_SECURITY.getDefaultValue()), false, TRUE_FALSE), new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.USE_DEFAULT_GSS_CREDENTIAL.toString(), - Boolean.toString(SQLServerDriverBooleanProperty.USE_DEFAULT_GSS_CREDENTIAL.getDefaultValue()), false, - TRUE_FALSE), + Boolean.toString(SQLServerDriverBooleanProperty.USE_DEFAULT_GSS_CREDENTIAL.getDefaultValue()), + false, TRUE_FALSE), new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.KEY_STORE_AUTHENTICATION.toString(), SQLServerDriverStringProperty.KEY_STORE_AUTHENTICATION.getDefaultValue(), false, new String[] {KeyStoreAuthentication.JAVA_KEYSTORE_PASSWORD.toString()}), @@ -1002,8 +1045,13 @@ public final class SQLServerDriver implements java.sql.Driver { new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.CONNECT_RETRY_COUNT.toString(), Integer.toString(SQLServerDriverIntProperty.CONNECT_RETRY_COUNT.getDefaultValue()), false, null), new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.CONNECT_RETRY_INTERVAL.toString(), - Integer.toString(SQLServerDriverIntProperty.CONNECT_RETRY_INTERVAL.getDefaultValue()), false, - null),}; + Integer.toString(SQLServerDriverIntProperty.CONNECT_RETRY_INTERVAL.getDefaultValue()), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.QUOTED_IDENTIFIER.toString(), + SQLServerDriverStringProperty.QUOTED_IDENTIFIER.getDefaultValue(), false, + new String[] {OnOffOption.OFF.toString(), OnOffOption.OFF.toString()}), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.CONCAT_NULL_YIELDS_NULL.toString(), + SQLServerDriverStringProperty.CONCAT_NULL_YIELDS_NULL.getDefaultValue(), false, + new String[] {OnOffOption.OFF.toString(), OnOffOption.OFF.toString()}),}; /** * Properties that can only be set by using Properties. Cannot set in connection string diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index 435dd286b6..9f1d00ea87 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -244,6 +244,8 @@ protected Object[][] getContents() { {"R_AADSecurePrincipalSecretPropertyDescription", "A Secret defined for a registered application which has been granted permission to the database connected."}, {"R_accessTokenCallbackClassPropertyDescription", "The class to instantiate as the SQLServerAccessTokenCallback for acquiring tokens."}, {"R_accessTokenCallbackPropertyDescription", "A SQLServerAccessTokenCallback object which is used to call a callback method to return an access token."}, + {"R_quotedIdentifierPropertyDescription", "Indicates whether quotedIdentifier property is set."}, + {"R_concatNullYieldsNullPropertyDescription", "Indicates whether concatNullYieldsNull property is set."}, {"R_noParserSupport", "An error occurred while instantiating the required parser. Error: \"{0}\""}, {"R_writeOnlyXML", "Cannot read from this SQLXML instance. This instance is for writing data only."}, {"R_dataHasBeenReadXML", "Cannot read from this SQLXML instance. The data has already been read."}, diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java index 7f481e974e..88d5c39c96 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java @@ -325,6 +325,12 @@ public void testDataSource() throws SQLServerException { ds.setKeyStorePrincipalId(stringPropValue); assertTrue(ds.getKeyStorePrincipalId().equals(stringPropValue)); + + ds.setQuotedIdentifier(stringPropValue); + assertTrue(ds.getQuotedIdentifier().equals(stringPropValue)); + + ds.setConcatNullYieldsNull(stringPropValue); + assertTrue(ds.getConcatNullYieldsNull().equals(stringPropValue)); } @Test @@ -462,6 +468,128 @@ public void testConnectionPoolGetTwice() throws SQLException { } } + /** + * Test connection properties: CONCAT_NULL_YIELDS_NULL with SQLServerXADataSource for new connection and pooled connection + * @throws SQLException + */ + @Test + public void testConcatNullYieldsNull() throws SQLException { + // Server default is CONCAT_NULL_YIELDS_NULL = ON + int expectedResultFlagOff = 0; + int expectedResultFlagOn = 1; + String sessionPropertyName = "CONCAT_NULL_YIELDS_NULL"; + + // Test for concatNullYieldsNull flag is OFF + SQLServerDataSource dsWithOff = new SQLServerDataSource(); + dsWithOff.setURL(connectionString); + dsWithOff.setConcatNullYieldsNull("OFF"); + testSessionPropertyValueHelper(dsWithOff.getConnection(), sessionPropertyName, expectedResultFlagOff); + // Test pooled connections + SQLServerXADataSource pdsWithOff = new SQLServerXADataSource(); + pdsWithOff.setURL(connectionString); + pdsWithOff.setConcatNullYieldsNull("OFF"); + + PooledConnection pcWithOff = pdsWithOff.getPooledConnection(); + try { + testSessionPropertyValueHelper(pcWithOff.getConnection(), sessionPropertyName, expectedResultFlagOff); + // Repeat getConnection to put the physical connection through a RESETCONNECTION + testSessionPropertyValueHelper(pcWithOff.getConnection(), sessionPropertyName, expectedResultFlagOff); + } finally { + if (null != pcWithOff) { + pcWithOff.close(); + } + } + // Test for concatNullYieldsNull flag is ON + SQLServerDataSource dsWithOn = new SQLServerDataSource(); + dsWithOn.setURL(connectionString); + dsWithOn.setConcatNullYieldsNull("ON"); + testSessionPropertyValueHelper(dsWithOn.getConnection(), sessionPropertyName, expectedResultFlagOn); + // Test pooled connections + SQLServerXADataSource pdsWithOn = new SQLServerXADataSource(); + pdsWithOn.setURL(connectionString); + pdsWithOn.setConcatNullYieldsNull("ON"); + + PooledConnection pcWithOn = pdsWithOn.getPooledConnection(); + try { + testSessionPropertyValueHelper(pcWithOn.getConnection(), sessionPropertyName, expectedResultFlagOn); + // Repeat getConnection to put the physical connection through a RESETCONNECTION + testSessionPropertyValueHelper(pcWithOn.getConnection(), sessionPropertyName, expectedResultFlagOn); + } finally { + if (null != pcWithOn) { + pcWithOn.close(); + } + } + } + + public void testSessionPropertyValueHelper(Connection con, String propName, int expectedResult) throws SQLException { + String sqlSelect = "SELECT SESSIONPROPERTY('" + propName + "')"; + try (Statement statement = con.createStatement()) { + try (ResultSet rs = statement.executeQuery(sqlSelect)) { + if (rs.next()) { + int actualResult = rs.getInt(1); + MessageFormat form1 = new MessageFormat( + TestResource.getResource("R_sessionPropertyFailed")); + Object[] msgArgs1 = {expectedResult, propName, actualResult}; + assertEquals(expectedResult, actualResult, form1.format(msgArgs1)); + } else { + assertTrue(false, "Expected row of data was not found."); + } + } + } + } + + /** + * Test connection properties: QUOTED_IDENTIFIER with SQLServerXADataSource for new connection and pooled connection + * @throws SQLException + */ + @Test + public void testQuptedIdentifier() throws SQLException { + // Server default is QUOTED_IDENTIFIER = ON + int expectedResultFlagOff = 0; + int expectedResultFlagOn = 1; + String sessionPropertyName = "QUOTED_IDENTIFIER"; + + //Test for quotedIdentifier flag is OFF + SQLServerDataSource dsWithOff = new SQLServerDataSource(); + dsWithOff.setURL(connectionString); + dsWithOff.setQuotedIdentifier("OFF"); + testSessionPropertyValueHelper(dsWithOff.getConnection(), sessionPropertyName, expectedResultFlagOff); + // Test pooled connections + SQLServerXADataSource pdsWithOff = new SQLServerXADataSource(); + pdsWithOff.setURL(connectionString); + pdsWithOff.setQuotedIdentifier("OFF"); + PooledConnection pcWithOff = pdsWithOff.getPooledConnection(); + try { + testSessionPropertyValueHelper(pcWithOff.getConnection(), sessionPropertyName, expectedResultFlagOff); + // Repeat getConnection to put the physical connection through a RESETCONNECTION + testSessionPropertyValueHelper(pcWithOff.getConnection(), sessionPropertyName, expectedResultFlagOff); + } finally { + if (null != pcWithOff) { + pcWithOff.close(); + } + } + + // Test for quotedIdentifier flag is ON + SQLServerDataSource dsWithOn = new SQLServerDataSource(); + dsWithOn.setURL(connectionString); + dsWithOn.setQuotedIdentifier("ON"); + testSessionPropertyValueHelper(dsWithOn.getConnection(), sessionPropertyName, expectedResultFlagOn); + // Test pooled connections + SQLServerXADataSource pdsWithOn = new SQLServerXADataSource(); + pdsWithOn.setURL(connectionString); + pdsWithOn.setQuotedIdentifier("ON"); + PooledConnection pcWithOn = pdsWithOn.getPooledConnection(); + try { + testSessionPropertyValueHelper(pcWithOn.getConnection(), sessionPropertyName, expectedResultFlagOn); + // Repeat getConnection to put the physical connection through a RESETCONNECTION + testSessionPropertyValueHelper(pcWithOn.getConnection(), sessionPropertyName, expectedResultFlagOn); + } finally { + if (null != pcWithOn) { + pcWithOn.close(); + } + } + } + /** * Runs the `testConnectCountInLoginAndCorrectRetryCount` test several times with different values of * connectRetryCount. diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java index 29a5e21dd9..7a3b80e98c 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java @@ -216,5 +216,6 @@ protected Object[][] getContents() { {"R_expectedClassDoesNotMatchActualClass", "Expected column class {0} does not match actual column class {1} for column {2}."}, {"R_loginFailedMI", "Login failed for user ''"}, - {"R_MInotAvailable", "Managed Identity authentication is not available"},}; + {"R_MInotAvailable", "Managed Identity authentication is not available"}, + {"R_sessionPropertyFailed", "Expected {0} from server for session property {1} but got {2}."},}; } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/ParameterMetaDataTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/ParameterMetaDataTest.java index 0709c2e347..bff91e87a3 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/ParameterMetaDataTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/ParameterMetaDataTest.java @@ -77,11 +77,11 @@ public void compareStoredProcTest() throws SQLException { "SELECT cInt FROM " + tableName + " WHERE (cInt + (3 - 5)) = ?", "SELECT cInt FROM " + tableName + " WHERE " + tableName + ".[cInt] = ?", "SELECT cInt FROM " + tableName + " WHERE ? = " + tableName + ".[cInt]", - "WITH t1(cInt) AS (SELECT 1), t2(cInt) AS (SELECT 2) SELECT * FROM t1 JOIN t2 ON [t1].\"cInt\" = \"t2\".[cInt] WHERE \"t1\".[cInt] = [t2].\"cInt\" + ?", + "WITH t1(cInt) AS (SELECT 1), t2(cInt) AS (SELECT 2) SELECT * FROM t1 JOIN t2 ON [t1].[cInt] = [t2].[cInt] WHERE [t1].[cInt] = [t2].[cInt] + ?", "INSERT INTO " + tableName + "(cInt,cFloat) SELECT 1,1.5 WHERE 1 > ?", "WITH t1(cInt) AS (SELECT 1), t2(cInt) AS (SELECT 2), t3(cInt) AS (SELECT 3) SELECT * FROM t1,t2,t3 WHERE t1.cInt >= ?", - "SELECT (1),2,[cInt],\"cFloat\" FROM " + tableName + " WHERE cNvarchar LIKE ?", - "WITH t1(cInt) AS (SELECT 1) SELECT * FROM \"t1\""); + "SELECT (1),2,[cInt],[cFloat] FROM " + tableName + " WHERE cNvarchar LIKE ?", + "WITH t1(cInt) AS (SELECT 1) SELECT * FROM [t1]"); l.forEach(this::compareFmtAndSp); }