diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index c8232d71a..432aa9e32 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -83,8 +83,6 @@ // Note all the public functions in this class also need to be defined in SQLServerConnectionPoolProxy. public class SQLServerConnection implements ISQLServerConnection { - boolean contextIsAlreadyChanged = false; - boolean contextChanged = false; long timerExpire; boolean attemptRefreshTokenLocked = false; @@ -276,15 +274,20 @@ static ParsedSQLCacheItem parseAndCacheSQL(Sha1HashKey key, String sql) throws return cacheItem; } + /** Default size for prepared statement caches */ + static final int DEFAULT_STATEMENT_POOLING_CACHE_SIZE = 0; + /** Size of the prepared statement handle cache */ - private int statementPoolingCacheSize = 10; + private int statementPoolingCacheSize = DEFAULT_STATEMENT_POOLING_CACHE_SIZE; - /** Default size for prepared statement caches */ - static final int DEFAULT_STATEMENT_POOLING_CACHE_SIZE = 10; /** Cache of prepared statement handles */ private ConcurrentLinkedHashMap preparedStatementHandleCache; /** Cache of prepared statement parameter metadata */ private ConcurrentLinkedHashMap parameterMetadataCache; + /** + * Checks whether statement pooling is enabled or disabled. The default is set to true; + */ + private boolean disableStatementPooling = true; /** * Find statement parameters. @@ -925,17 +928,10 @@ final boolean attachConnId() { connectionlogger.severe(message); throw new UnsupportedOperationException(message); } - + // Caching turned on? - if (0 < this.getStatementPoolingCacheSize()) { - preparedStatementHandleCache = new Builder() - .maximumWeightedCapacity(getStatementPoolingCacheSize()) - .listener(new PreparedStatementCacheEvictionListener()) - .build(); - - parameterMetadataCache = new Builder() - .maximumWeightedCapacity(getStatementPoolingCacheSize()) - .build(); + if (!this.getDisableStatementPooling() && 0 < this.getStatementPoolingCacheSize()) { + prepareCache(); } } @@ -1437,10 +1433,8 @@ Connection connectInternal(Properties propsIn, sPropKey = SQLServerDriverBooleanProperty.DISABLE_STATEMENT_POOLING.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); if (null != sPropValue) { - // If disabled set cache size to 0 if disabled. - if(booleanPropertyOn(sPropKey, sPropValue)) - this.setStatementPoolingCacheSize(0); - } + setDisableStatementPooling(booleanPropertyOn(sPropKey, sPropValue)); + } sPropKey = SQLServerDriverBooleanProperty.INTEGRATED_SECURITY.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); @@ -3080,8 +3074,6 @@ final void poolCloseEventNotify() throws SQLServerException { checkClosed(); if (catalog != null) { connectionCommand("use " + Util.escapeSQLId(catalog), "setCatalog"); - contextIsAlreadyChanged = true; - contextChanged = true; sCatalog = catalog; } loggerExternal.exiting(getClassNameLogging(), "setCatalog"); @@ -5692,6 +5684,26 @@ final void unprepareUnreferencedPreparedStatementHandles(boolean force) { } } + /** + * Returns true if statement pooling is disabled. + * + * @return + */ + public boolean getDisableStatementPooling() { + return this.disableStatementPooling; + } + + /** + * Sets statement pooling to true or false; + * + * @param value + */ + public void setDisableStatementPooling(boolean value) { + this.disableStatementPooling = value; + if (!value && 0 < this.getStatementPoolingCacheSize()) { + prepareCache(); + } + } /** * Returns the size of the prepared statement cache for this connection. A value less than 1 means no cache. @@ -5699,7 +5711,7 @@ final void unprepareUnreferencedPreparedStatementHandles(boolean force) { */ public int getStatementPoolingCacheSize() { return statementPoolingCacheSize; - } + } /** * Returns the current number of pooled prepared statement handles. @@ -5717,25 +5729,40 @@ public int getStatementHandleCacheEntryCount() { * @return Returns the current setting per the description. */ public boolean isStatementPoolingEnabled() { - return null != preparedStatementHandleCache && 0 < this.getStatementPoolingCacheSize(); + return null != preparedStatementHandleCache && 0 < this.getStatementPoolingCacheSize() && !this.getDisableStatementPooling(); } /** - * Specifies the size of the prepared statement cache for this conection. A value less than 1 means no cache. - * @param value The new cache size. + * Specifies the size of the prepared statement cache for this connection. A value less than 1 means no cache. + * + * @param value + * The new cache size. * */ public void setStatementPoolingCacheSize(int value) { - if (value != this.statementPoolingCacheSize) { - value = Math.max(0, value); - statementPoolingCacheSize = value; - - if (null != preparedStatementHandleCache) - preparedStatementHandleCache.setCapacity(value); + value = Math.max(0, value); + statementPoolingCacheSize = value; - if (null != parameterMetadataCache) - parameterMetadataCache.setCapacity(value); + if (!this.disableStatementPooling && value > 0) { + prepareCache(); } + if (null != preparedStatementHandleCache) + preparedStatementHandleCache.setCapacity(value); + + if (null != parameterMetadataCache) + parameterMetadataCache.setCapacity(value); + } + + /** + * Internal method to prepare the cache handle + * @param value + */ + private void prepareCache() { + preparedStatementHandleCache = new Builder().maximumWeightedCapacity(getStatementPoolingCacheSize()) + .listener(new PreparedStatementCacheEvictionListener()).build(); + + parameterMetadataCache = new Builder().maximumWeightedCapacity(getStatementPoolingCacheSize()) + .build(); } /** Get a parameter metadata cache entry if statement pooling is enabled */ @@ -5788,12 +5815,6 @@ final void evictCachedPreparedStatementHandle(PreparedStatementHandle handle) { preparedStatementHandleCache.remove(handle.getKey()); } - final void clearCachedPreparedStatementHandle() { - if (null != preparedStatementHandleCache) { - preparedStatementHandleCache.clear(); - } - } - // Handle closing handles when removed from cache. final class PreparedStatementCacheEvictionListener implements EvictionListener { public void onEviction(Sha1HashKey key, PreparedStatementHandle handle) { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java index 3117ec403..80d3a234d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java @@ -169,8 +169,6 @@ public void run() { if (wrappedConnection.getConnectionLogger().isLoggable(Level.FINER)) wrappedConnection.getConnectionLogger().finer(toString() + " Connection proxy closed "); - // clear cached prepared statement handle on this connection - wrappedConnection.clearCachedPreparedStatementHandle(); wrappedConnection.poolCloseEventNotify(); wrappedConnection = null; } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java index ebe4fee35..2f354f1c4 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java @@ -744,7 +744,7 @@ public int getServerPreparedStatementDiscardThreshold() { } /** - * Specifies the size of the prepared statement cache for this conection. A value less than 1 means no cache. + * Specifies the size of the prepared statement cache for this connection. A value less than 1 means no cache. * * @param statementPoolingCacheSize * Changes the setting per the description. @@ -754,7 +754,7 @@ public void setStatementPoolingCacheSize(int statementPoolingCacheSize) { } /** - * Returns the size of the prepared statement cache for this conection. A value less than 1 means no cache. + * Returns the size of the prepared statement cache for this connection. A value less than 1 means no cache. * * @return Returns the current setting per the description. */ @@ -762,6 +762,24 @@ public int getStatementPoolingCacheSize() { int defaultSize = SQLServerDriverIntProperty.STATEMENT_POOLING_CACHE_SIZE.getDefaultValue(); return getIntProperty(connectionProps, SQLServerDriverIntProperty.STATEMENT_POOLING_CACHE_SIZE.toString(), defaultSize); } + + /** + * Sets the statement pooling to true or false + * @param disableStatementPooling + */ + public void setDisableStatementPooling(boolean disableStatementPooling) { + setBooleanProperty(connectionProps, SQLServerDriverBooleanProperty.DISABLE_STATEMENT_POOLING.toString(), disableStatementPooling); + } + + /** + * Returns true if statement pooling is disabled. + * @return + */ + public boolean getDisableStatementPooling() { + boolean defaultValue = SQLServerDriverBooleanProperty.DISABLE_STATEMENT_POOLING.getDefaultValue(); + return getBooleanProperty(connectionProps, SQLServerDriverBooleanProperty.DISABLE_STATEMENT_POOLING.toString(), + defaultValue); + } /** * Setting the socket timeout diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java index 940d21907..f04f0e7f4 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java @@ -340,7 +340,7 @@ public String toString() { enum SQLServerDriverBooleanProperty { - DISABLE_STATEMENT_POOLING ("disableStatementPooling", false), + DISABLE_STATEMENT_POOLING ("disableStatementPooling", true), ENCRYPT ("encrypt", false), INTEGRATED_SECURITY ("integratedSecurity", false), LAST_UPDATE_COUNT ("lastUpdateCount", true), diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index bdffeab96..0ec3d4aab 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -51,8 +51,6 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements ISQLServerPreparedStatement { /** Flag to indicate that it is an internal query to retrieve encryption metadata. */ boolean isInternalEncryptionQuery = false; - - boolean definitionChanged = false; /** delimiter for multiple statements in a single batch */ private static final int BATCH_STATEMENT_DELIMITER_TDS_71 = 0x80; @@ -330,14 +328,7 @@ private boolean buildPreparedStrings(Parameter[] params, boolean renewDefinition) throws SQLServerException { String newTypeDefinitions = buildParamTypeDefinitions(params, renewDefinition); if (null != preparedTypeDefinitions && newTypeDefinitions.equals(preparedTypeDefinitions)) - return false; - - if(preparedTypeDefinitions == null) { - definitionChanged = false; - } - else { - definitionChanged = true; - } + return false; preparedTypeDefinitions = newTypeDefinitions; @@ -498,8 +489,6 @@ final void processResponse(TDSReader tdsReader) throws SQLServerException { final void doExecutePreparedStatement(PrepStmtExecCmd command) throws SQLServerException { resetForReexecute(); - definitionChanged = false; - // If this request might be a query (as opposed to an update) then make // sure we set the max number of rows and max field size for any ResultSet // that may be returned. @@ -539,19 +528,33 @@ final void doExecutePreparedStatement(PrepStmtExecCmd command) throws SQLServerE } String dbName = connection.getSCatalog(); - if (reuseCachedHandle(hasNewTypeDefinitions, false, dbName)) { - hasNewTypeDefinitions = false; - } + boolean needsPrepare = false; + // Retry execution if existing handle could not be re-used. + for (int attempt = 1; attempt <= 2; ++attempt) { + try { + // Re-use handle if available, requires parameter definitions which are not available until here. + if (reuseCachedHandle(hasNewTypeDefinitions, 1 < attempt, dbName)) { + hasNewTypeDefinitions = false; + } - // Start the request and detach the response reader so that we can - // continue using it after we return. - TDSWriter tdsWriter = command.startRequest(TDS.PKT_RPC); + // Start the request and detach the response reader so that we can + // continue using it after we return. + TDSWriter tdsWriter = command.startRequest(TDS.PKT_RPC); - doPrepExec(tdsWriter, inOutParam, hasNewTypeDefinitions, hasExistingTypeDefinitions); + needsPrepare = doPrepExec(tdsWriter, inOutParam, hasNewTypeDefinitions, hasExistingTypeDefinitions); - ensureExecuteResultsReader(command.startResponse(getIsResponseBufferingAdaptive())); - startResults(); - getNextResult(); + ensureExecuteResultsReader(command.startResponse(getIsResponseBufferingAdaptive())); + startResults(); + getNextResult(); + } + catch (SQLException e) { + if (retryBasedOnFailedReuseOfCachedHandle(e, attempt)) + continue; + else + throw e; + } + break; + } if (EXECUTE_QUERY == executeMethod && null == resultSet) { SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_noResultset"), null, true); @@ -560,6 +563,17 @@ else if (EXECUTE_UPDATE == executeMethod && null != resultSet) { SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_resultsetGeneratedForUpdate"), null, false); } } + + /** Should the execution be retried because the re-used cached handle could not be re-used due to server side state changes? */ + private boolean retryBasedOnFailedReuseOfCachedHandle(SQLException e, + int attempt) { + // Only retry based on these error codes and if statementPooling is enabled: + // 586: The prepared statement handle %d is not valid in this context. Please verify that current database, user default schema, and + // ANSI_NULLS and QUOTED_IDENTIFIER set options are not changed since the handle is prepared. + // 8179: Could not find prepared statement with handle %d. + // 99586: Error used for testing. + return 1 == attempt && (586 == e.getErrorCode() || 8179 == e.getErrorCode() || 99586 == e.getErrorCode()) && connection.isStatementPoolingEnabled(); + } /** * Consume the OUT parameter for the statement object itself. @@ -915,18 +929,6 @@ private void getParameterEncryptionMetadata(Parameter[] params) throws SQLServer /** Manage re-using cached handles */ private boolean reuseCachedHandle(boolean hasNewTypeDefinitions, boolean discardCurrentCacheItem, String dbName) { - if (definitionChanged || connection.contextChanged) { - prepStmtHandle = -1; // so that hasPreparedStatementHandle() also returns false - - if (connection.contextChanged) { - connection.contextChanged = false; - connection.contextIsAlreadyChanged = false; - connection.clearCachedPreparedStatementHandle(); - } - - return false; - } - // No re-use of caching for cursorable statements (statements that WILL use sp_cursor*) if (isCursorable(executeMethod)) return false; @@ -2576,7 +2578,7 @@ final void doExecutePreparedStatementBatch(PrepStmtBatchExecCmd batchCommand) th // Create the parameter array that we'll use for all the items in this batch. Parameter[] batchParam = new Parameter[inOutParam.length]; - definitionChanged = false; + /* TDSWriter tdsWriter = null; while (numBatchesExecuted < numBatches) { @@ -2595,122 +2597,148 @@ final void doExecutePreparedStatementBatch(PrepStmtBatchExecCmd batchCommand) th */ TDSWriter tdsWriter = null; - try { - while (numBatchesExecuted < numBatches) { - // Fill in the parameter values for this batch - Parameter paramValues[] = batchParamValues.get(numBatchesPrepared); - assert paramValues.length == batchParam.length; - System.arraycopy(paramValues, 0, batchParam, 0, paramValues.length); - - boolean hasExistingTypeDefinitions = preparedTypeDefinitions != null; - boolean hasNewTypeDefinitions = buildPreparedStrings(batchParam, false); - - // Get the encryption metadata for the first batch only. - if ((0 == numBatchesExecuted) && (Util.shouldHonorAEForParameters(stmtColumnEncriptionSetting, connection)) && (0 < batchParam.length) - && !isInternalEncryptionQuery && !encryptionMetadataIsRetrieved) { - getParameterEncryptionMetadata(batchParam); - - // fix an issue when inserting unicode into non-encrypted nchar column using setString() and AE is on on Connection - buildPreparedStrings(batchParam, true); - - // Save the crypto metadata retrieved for the first batch. We will re-use these for the rest of the batches. - for (Parameter aBatchParam : batchParam) { - cryptoMetaBatch.add(aBatchParam.cryptoMeta); - } - } + while (numBatchesExecuted < numBatches) { + // Fill in the parameter values for this batch + Parameter paramValues[] = batchParamValues.get(numBatchesPrepared); + assert paramValues.length == batchParam.length; + System.arraycopy(paramValues, 0, batchParam, 0, paramValues.length); - // Update the crypto metadata for this batch. - if (0 < numBatchesExecuted) { - // cryptoMetaBatch will be empty for non-AE connections/statements. - for (int i = 0; i < cryptoMetaBatch.size(); i++) { - batchParam[i].cryptoMeta = cryptoMetaBatch.get(i); - } - } - - String dbName = connection.getSCatalog(); - if (reuseCachedHandle(hasNewTypeDefinitions, false, dbName)) { - hasNewTypeDefinitions = false; - } + boolean hasExistingTypeDefinitions = preparedTypeDefinitions != null; + boolean hasNewTypeDefinitions = buildPreparedStrings(batchParam, false); - if (numBatchesExecuted < numBatchesPrepared) { - // assert null != tdsWriter; - tdsWriter.writeByte((byte) nBatchStatementDelimiter); + // Get the encryption metadata for the first batch only. + if ((0 == numBatchesExecuted) && (Util.shouldHonorAEForParameters(stmtColumnEncriptionSetting, connection)) && (0 < batchParam.length) + && !isInternalEncryptionQuery && !encryptionMetadataIsRetrieved) { + getParameterEncryptionMetadata(batchParam); + + // fix an issue when inserting unicode into non-encrypted nchar column using setString() and AE is on on Connection + buildPreparedStrings(batchParam, true); + + // Save the crypto metadata retrieved for the first batch. We will re-use these for the rest of the batches. + for (Parameter aBatchParam : batchParam) { + cryptoMetaBatch.add(aBatchParam.cryptoMeta); } - else { - resetForReexecute(); - tdsWriter = batchCommand.startRequest(TDS.PKT_RPC); + } + + // Update the crypto metadata for this batch. + if (0 < numBatchesExecuted) { + // cryptoMetaBatch will be empty for non-AE connections/statements. + for (int i = 0; i < cryptoMetaBatch.size(); i++) { + batchParam[i].cryptoMeta = cryptoMetaBatch.get(i); } + } - // If we have to (re)prepare the statement then we must execute it so - // that we get back a (new) prepared statement handle to use to - // execute additional batches. - // - // We must always prepare the statement the first time through. - // But we may also need to reprepare the statement if, for example, - // the size of a batch's string parameter values changes such - // that repreparation is necessary. - ++numBatchesPrepared; - - if (doPrepExec(tdsWriter, batchParam, hasNewTypeDefinitions, hasExistingTypeDefinitions) || numBatchesPrepared == numBatches) { - ensureExecuteResultsReader(batchCommand.startResponse(getIsResponseBufferingAdaptive())); - - while (numBatchesExecuted < numBatchesPrepared) { - // NOTE: - // When making changes to anything below, consider whether similar changes need - // to be made to Statement batch execution. - - startResults(); - - try { - // Get the first result from the batch. If there is no result for this batch - // then bail, leaving EXECUTE_FAILED in the current and remaining slots of - // the update count array. - if (!getNextResult()) - return; - - // If the result is a ResultSet (rather than an update count) then throw an - // exception for this result. The exception gets caught immediately below and - // translated into (or added to) a BatchUpdateException. - if (null != resultSet) { - SQLServerException.makeFromDriverError(connection, this, - SQLServerException.getErrString("R_resultsetGeneratedForUpdate"), null, false); - } - } - catch (SQLServerException e) { - // If the failure was severe enough to close the connection or roll back a - // manual transaction, then propagate the error up as a SQLServerException - // now, rather than continue with the batch. - if (connection.isSessionUnAvailable() || connection.rolledBackTransaction()) - throw e; - - // Otherwise, the connection is OK and the transaction is still intact, - // so just record the failure for the particular batch item. - updateCount = Statement.EXECUTE_FAILED; - if (null == batchCommand.batchException) - batchCommand.batchException = e; - } + String dbName = connection.getSCatalog(); + boolean needsPrepare = false; + // Retry execution if existing handle could not be re-used. + for (int attempt = 1; attempt <= 2; ++attempt) { + try { - // In batch execution, we have a special update count - // to indicate that no information was returned - batchCommand.updateCounts[numBatchesExecuted] = (-1 == updateCount) ? Statement.SUCCESS_NO_INFO : updateCount; - processBatch(); + // Re-use handle if available, requires parameter definitions which are not available until here. + if (reuseCachedHandle(hasNewTypeDefinitions, 1 < attempt, dbName)) { + hasNewTypeDefinitions = false; + } - numBatchesExecuted++; + if (numBatchesExecuted < numBatchesPrepared) { + // assert null != tdsWriter; + tdsWriter.writeByte((byte) nBatchStatementDelimiter); + } + else { + resetForReexecute(); + tdsWriter = batchCommand.startRequest(TDS.PKT_RPC); } - // Only way to proceed with preparing the next set of batches is if - // we successfully executed the previously prepared set. - assert numBatchesExecuted == numBatchesPrepared; + // If we have to (re)prepare the statement then we must execute it so + // that we get back a (new) prepared statement handle to use to + // execute additional batches. + // + // We must always prepare the statement the first time through. + // But we may also need to reprepare the statement if, for example, + // the size of a batch's string parameter values changes such + // that repreparation is necessary. + ++numBatchesPrepared; + needsPrepare = doPrepExec(tdsWriter, batchParam, hasNewTypeDefinitions, hasExistingTypeDefinitions); + if ( needsPrepare || numBatchesPrepared == numBatches) { + ensureExecuteResultsReader(batchCommand.startResponse(getIsResponseBufferingAdaptive())); + + boolean retry = false; + while (numBatchesExecuted < numBatchesPrepared) { + // NOTE: + // When making changes to anything below, consider whether similar changes need + // to be made to Statement batch execution. + + startResults(); + + try { + // Get the first result from the batch. If there is no result for this batch + // then bail, leaving EXECUTE_FAILED in the current and remaining slots of + // the update count array. + if (!getNextResult()) + return; + + // If the result is a ResultSet (rather than an update count) then throw an + // exception for this result. The exception gets caught immediately below and + // translated into (or added to) a BatchUpdateException. + if (null != resultSet) { + SQLServerException.makeFromDriverError(connection, this, + SQLServerException.getErrString("R_resultsetGeneratedForUpdate"), null, false); + } + } + catch (SQLServerException e) { + // If the failure was severe enough to close the connection or roll back a + // manual transaction, then propagate the error up as a SQLServerException + // now, rather than continue with the batch. + if (connection.isSessionUnAvailable() || connection.rolledBackTransaction()) + throw e; + + // Retry if invalid handle exception. + if (retryBasedOnFailedReuseOfCachedHandle(e, attempt)) { + // reset number of batches prepare + numBatchesPrepared = numBatchesExecuted; + retry = true; + break; + } + + // Otherwise, the connection is OK and the transaction is still intact, + // so just record the failure for the particular batch item. + updateCount = Statement.EXECUTE_FAILED; + if (null == batchCommand.batchException) + batchCommand.batchException = e; + + } + + // In batch execution, we have a special update count + // to indicate that no information was returned + batchCommand.updateCounts[numBatchesExecuted] = (-1 == updateCount) ? Statement.SUCCESS_NO_INFO : updateCount; + processBatch(); + + numBatchesExecuted++; + } + if (retry) + continue; + + // Only way to proceed with preparing the next set of batches is if + // we successfully executed the previously prepared set. + assert numBatchesExecuted == numBatchesPrepared; + } } - } - } - catch (SQLServerException e) { - // throw the initial batchException - if (null != batchCommand.batchException) { - throw batchCommand.batchException; - } - else { - throw e; + catch (SQLException e) { + if (retryBasedOnFailedReuseOfCachedHandle(e, attempt) && connection.isStatementPoolingEnabled()) { + // Reset number of batches prepared. + numBatchesPrepared = numBatchesExecuted; + continue; + } + else if (null != batchCommand.batchException) { + // if batch exception occurred, loop out to throw the initial batchException + numBatchesExecuted = numBatchesPrepared; + attempt++; + continue; + } + else { + throw e; + } + } + break; } } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java index 696cfa6c5..7d7608433 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java @@ -800,21 +800,6 @@ final void setMaxRowsAndMaxFieldSize() throws SQLServerException { } } - /* - * check if context has been changed by monitoring statment call on the connection, since context is connection based. - */ - void checkIfContextChanged(String sql) { - if (connection.contextIsAlreadyChanged) { - connection.contextChanged = true; - return; - } - else if (sql.toUpperCase().contains("ANSI_NULLS") || sql.toUpperCase().contains("QUOTED_IDENTIFIER") || sql.toUpperCase().contains("USE") - || sql.toUpperCase().contains("DEFAULT_SCHEMA")) { - connection.contextIsAlreadyChanged = true; - connection.contextChanged = true; - } - } - final void doExecuteStatement(StmtExecCmd execCmd) throws SQLServerException { resetForReexecute(); @@ -826,8 +811,6 @@ final void doExecuteStatement(StmtExecCmd execCmd) throws SQLServerException { // call syntax is rewritten here as SQL exec syntax. String sql = ensureSQLSyntax(execCmd.sql); - checkIfContextChanged(sql); - // If this request might be a query (as opposed to an update) then make // sure we set the max number of rows and max field size for any ResultSet // that may be returned. @@ -931,9 +914,7 @@ private void doExecuteStatementBatch(StmtBatchExecCmd execCmd) throws SQLServerE tdsWriter.writeString(batchIter.next()); while (batchIter.hasNext()) { tdsWriter.writeString(" ; "); - String sql = batchIter.next(); - tdsWriter.writeString(sql); - checkIfContextChanged(sql); + tdsWriter.writeString(batchIter.next()); } // Start the response diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java index 11edce8c6..58c7969ab 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java @@ -8,10 +8,10 @@ package com.microsoft.sqlserver.jdbc.unit.statement; import static java.util.concurrent.TimeUnit.SECONDS; -import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.fail; import java.sql.BatchUpdateException; @@ -173,7 +173,7 @@ public void testStatementPooling() throws SQLException { } } - try (SQLServerConnection con = (SQLServerConnection) DriverManager.getConnection(connectionString)) { + try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { // Test behvaior with statement pooling. con.setStatementPoolingCacheSize(10); @@ -239,9 +239,10 @@ public void testStatementPooling() throws SQLException { } } } - + try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { // Test behvaior with statement pooling. + con.setDisableStatementPooling(false); con.setStatementPoolingCacheSize(10); String lookupUniqueifier = UUID.randomUUID().toString(); @@ -304,11 +305,12 @@ public void testStatementPoolingEviction() throws SQLException { for (int testNo = 0; testNo < 2; ++testNo) { try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { - int cacheSize = 10; int discardedStatementCount = testNo == 0 ? 5 /*batched unprepares*/ : 0 /*regular unprepares*/; - con.setStatementPoolingCacheSize(cacheSize); + // enabling caching + con.setDisableStatementPooling(false); + con.setStatementPoolingCacheSize(cacheSize); con.setServerPreparedStatementDiscardThreshold(discardedStatementCount); String lookupUniqueifier = UUID.randomUUID().toString(); @@ -452,7 +454,7 @@ public void testStatementPoolingPreparedStatementExecAndUnprepareConfig() throws SQLServerDataSource dataSource = new SQLServerDataSource(); dataSource.setURL(connectionString); // Verify defaults. - assertTrue(0 < dataSource.getStatementPoolingCacheSize()); + assertTrue(0 == dataSource.getStatementPoolingCacheSize()); // Verify change dataSource.setStatementPoolingCacheSize(0); assertSame(0, dataSource.getStatementPoolingCacheSize()); @@ -469,12 +471,34 @@ public void testStatementPoolingPreparedStatementExecAndUnprepareConfig() throws // Test disableStatementPooling String connectionStringDisableStatementPooling = connectionString + ";disableStatementPooling=true;"; SQLServerConnection connectionDisableStatementPooling = (SQLServerConnection)DriverManager.getConnection(connectionStringDisableStatementPooling); - assertSame(0, connectionDisableStatementPooling.getStatementPoolingCacheSize()); + connectionDisableStatementPooling.setStatementPoolingCacheSize(10); // to turn on caching and check if disableStatementPooling is true, even setting cachesize won't matter and will disable it. + assertSame(10, connectionDisableStatementPooling.getStatementPoolingCacheSize()); assertTrue(!connectionDisableStatementPooling.isStatementPoolingEnabled()); String connectionStringEnableStatementPooling = connectionString + ";disableStatementPooling=false;"; SQLServerConnection connectionEnableStatementPooling = (SQLServerConnection)DriverManager.getConnection(connectionStringEnableStatementPooling); - assertTrue(0 < connectionEnableStatementPooling.getStatementPoolingCacheSize()); - + connectionEnableStatementPooling.setStatementPoolingCacheSize(10); // to turn on caching. + assertTrue(0 < connectionEnableStatementPooling.getStatementPoolingCacheSize()); // for now, it won't affect if disable is false or true. Since statementPoolingCacheSize is set to 0 as default. + //If only disableStatementPooling is set to true, it makes sure that statementPoolingCacheSize is zero, thus disabling the prepared statement metadata caching. + assertTrue(connectionEnableStatementPooling.isStatementPoolingEnabled()); + + String connectionPropertyStringEnableStatementPooling = connectionString + ";disableStatementPooling=false;statementPoolingCacheSize=10"; + SQLServerConnection connectionPropertyEnableStatementPooling = (SQLServerConnection)DriverManager.getConnection(connectionPropertyStringEnableStatementPooling); + assertTrue(0 < connectionPropertyEnableStatementPooling.getStatementPoolingCacheSize()); // for now, it won't affect if disable is false or true. Since statementPoolingCacheSize is set to 0 as default. + //If only disableStatementPooling is set to true, it makes sure that statementPoolingCacheSize is zero, thus disabling the prepared statement metadata caching. + assertTrue(connectionPropertyEnableStatementPooling.isStatementPoolingEnabled()); + + String connectionPropertyStringDisableStatementPooling = connectionString + ";disableStatementPooling=true;statementPoolingCacheSize=10"; + SQLServerConnection connectionPropertyDisableStatementPooling = (SQLServerConnection)DriverManager.getConnection(connectionPropertyStringDisableStatementPooling); + assertTrue(0 < connectionPropertyDisableStatementPooling.getStatementPoolingCacheSize()); // for now, it won't affect if disable is false or true. Since statementPoolingCacheSize is set to 0 as default. + //If only disableStatementPooling is set to true, it makes sure that statementPoolingCacheSize is zero, thus disabling the prepared statement metadata caching. + assertTrue(!connectionPropertyDisableStatementPooling.isStatementPoolingEnabled()); + + String connectionPropertyStringDisableStatementPooling2 = connectionString + ";disableStatementPooling=false;statementPoolingCacheSize=0"; + SQLServerConnection connectionPropertyDisableStatementPooling2 = (SQLServerConnection)DriverManager.getConnection(connectionPropertyStringDisableStatementPooling2); + assertTrue(0 == connectionPropertyDisableStatementPooling2.getStatementPoolingCacheSize()); // for now, it won't affect if disable is false or true. Since statementPoolingCacheSize is set to 0 as default. + //If only disableStatementPooling is set to true, it makes sure that statementPoolingCacheSize is zero, thus disabling the prepared statement metadata caching. + assertTrue(!connectionPropertyDisableStatementPooling2.isStatementPoolingEnabled()); + // Test EnablePrepareOnFirstPreparedStatementCall String connectionStringNoExecuteSQL = connectionString + ";enablePrepareOnFirstPreparedStatementCall=true;"; SQLServerConnection connectionNoExecuteSQL = (SQLServerConnection)DriverManager.getConnection(connectionStringNoExecuteSQL); @@ -545,54 +569,4 @@ public void testStatementPoolingPreparedStatementExecAndUnprepareConfig() throws assertSame(0, con.getDiscardedServerPreparedStatementCount()); } } - - /** - * Validate the right behavior for EnablePrepareOnFirstPreparedStatementCall with - * statement pooling turned off. - * - * @throws SQLException - */ - @Test - public void testEnablePrepareOnFirstPreparedStatementCallWithStatementPoolingOff() throws SQLException { - - try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { - - // Turn off use of prepared statement cache. - con.setStatementPoolingCacheSize(0); - // Disable EnablePrepareOnFirstPreparedStatementCall (default) - con.setEnablePrepareOnFirstPreparedStatementCall(false); - - String query = "/*testEnablePrepareOnFirstPreparedStatementCallWithStatementPoolingOff*/SELECT * FROM sys.objects;"; - - // Verify first use is never prepared. - for(int i = 0; i < 10; ++i) - { - try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query)) { - pstmt.execute(); // sp_executesql - pstmt.getMoreResults(); // Make sure handle is updated. - - // Validate no handle was created. - assertTrue(0 >= pstmt.getPreparedStatementHandle()); - } - } - - // Verify second use is prepared. - for(int i = 0; i < 10; ++i) - { - try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query)) { - pstmt.execute(); // sp_executesql - pstmt.getMoreResults(); // Make sure handle is updated. - - // Validate no handle was created. - assertTrue(0 >= pstmt.getPreparedStatementHandle()); - - pstmt.execute(); // sp_prepexec - pstmt.getMoreResults(); // Make sure handle is updated. - - // Validate handle was created. - assertTrue(0 < pstmt.getPreparedStatementHandle()); - } - } - } - } -} +} \ No newline at end of file