diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java index 62fdeda8b..c00884cc5 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 ed7f21efc..f8883cc17 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -3527,6 +3527,38 @@ else if (0 == requestedPacketSize) state = State.OPENED; + // 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; + } + } + // Socket timeout is bounded by loginTimeout during the login phase. // Reset socket timeout back to the original value. tdsChannel.resetTcpSocketTimeout(); @@ -4782,6 +4814,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 61b9f8f0c..6267d652d 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 a265d8099..aa1c0c5c0 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 942bfdc26..936c7ce97 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 7f481e974..b95064952 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,126 @@ 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 { + SQLServerDataSource ds = new SQLServerDataSource(); + ds.setURL(connectionString); + ds.setConcatNullYieldsNull("OFF"); + int expectedResultForNewConnection = 0; + // Server default is CONCAT_NULL_YIELDS_NULL = ON + int expectedResultForPooledConnection = 1; + + String sqlSelect = "SELECT SESSIONPROPERTY('CONCAT_NULL_YIELDS_NULL')"; + + try (Connection con = ds.getConnection(); Statement stmt = con.createStatement()) { + try (ResultSet rs = stmt.executeQuery(sqlSelect)) { + if (rs.next()) { + assertEquals(expectedResultForNewConnection, rs.getInt(1)); + } else { + assertTrue(false, "Expected row of data was not found."); + } + } + } + + // Test pooled connections + SQLServerXADataSource pds = new SQLServerXADataSource(); + pds.setURL(connectionString); + pds.setConcatNullYieldsNull("OFF"); + + PooledConnection pc = pds.getPooledConnection(); + try { + try (Connection con = pc.getConnection(); Statement statement = con.createStatement()) { + try (ResultSet rs = statement.executeQuery(sqlSelect)) { + if (rs.next()) { + assertEquals(expectedResultForNewConnection, rs.getInt(1)); + } else { + assertTrue(false, "Expected row of data was not found."); + } + } + } + // Repeat getConnection to put the physical connection through a RESETCONNECTION + try (Connection con = pc.getConnection(); Statement statement = con.createStatement()) { + try (ResultSet rs = statement.executeQuery(sqlSelect)) { + if (rs.next()) { + assertEquals(expectedResultForPooledConnection, rs.getInt(1)); + } else { + assertTrue(false, "Expected row of data was not found."); + } + } + } + } catch (Exception e) { + fail(TestResource.getResource("R_unexpectedErrorMessage") + e.getMessage()); + } finally { + if (null != pc) { + pc.close(); + } + } + } + + /** + * Test connection properties: QUOTED_IDENTIFIER with SQLServerXADataSource for new connection and pooled connection + * @throws SQLException + */ + @Test + public void testQuptedIdentifier() throws SQLException { + SQLServerDataSource ds = new SQLServerDataSource(); + ds.setURL(connectionString); + ds.setQuotedIdentifier("OFF"); + int expectedResultForNewConnection = 0; + // Server default is QUOTED_IDENTIFIER = ON + int expectedResultForPooledConnection = 1; + + String sqlSelect = "SELECT SESSIONPROPERTY('QUOTED_IDENTIFIER')"; + + try (Connection con = ds.getConnection(); Statement stmt = con.createStatement()) { + try (ResultSet rs = stmt.executeQuery(sqlSelect)) { + if (rs.next()) { + assertEquals(expectedResultForNewConnection, rs.getInt(1)); + } else { + assertTrue(false, "Expected row of data was not found."); + } + } + } + + // Test pooled connections + SQLServerXADataSource pds = new SQLServerXADataSource(); + pds.setURL(connectionString); + pds.setQuotedIdentifier("OFF"); + + PooledConnection pc = pds.getPooledConnection(); + try { + try (Connection con = pc.getConnection(); Statement statement = con.createStatement()) { + try (ResultSet rs = statement.executeQuery(sqlSelect)) { + if (rs.next()) { + assertEquals(expectedResultForNewConnection, rs.getInt(1)); + } else { + assertTrue(false, "Expected row of data was not found."); + } + } + } + // Repeat getConnection to put the physical connection through a RESETCONNECTION + try (Connection con = pc.getConnection(); Statement statement = con.createStatement()) { + try (ResultSet rs = statement.executeQuery(sqlSelect)) { + if (rs.next()) { + assertEquals(expectedResultForPooledConnection, rs.getInt(1)); + } else { + assertTrue(false, "Expected row of data was not found."); + } + } + } + } catch (Exception e) { + fail(TestResource.getResource("R_unexpectedErrorMessage") + e.getMessage()); + } finally { + if (null != pc) { + pc.close(); + } + } + } + /** * Runs the `testConnectCountInLoginAndCorrectRetryCount` test several times with different values of * connectRetryCount. 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 0709c2e34..bff91e87a 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); }