Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
HHH-16458 Close JDBC statement when DeferredResultSetAccess fails to …
…execute a query
- Loading branch information
Showing
3 changed files
with
126 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
112 changes: 112 additions & 0 deletions
112
hibernate-core/src/test/java/org/hibernate/orm/test/query/QuerySqlExceptionTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
/* | ||
* Hibernate, Relational Persistence for Idiomatic Java | ||
* | ||
* License: GNU Lesser General Public License (LGPL), version 2.1 or later | ||
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html | ||
*/ | ||
package org.hibernate.orm.test.query; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||
|
||
import java.sql.SQLException; | ||
import java.time.LocalDate; | ||
import java.util.List; | ||
|
||
import org.hibernate.cfg.AvailableSettings; | ||
import org.hibernate.dialect.MySQLDialect; | ||
import org.hibernate.engine.spi.SessionFactoryImplementor; | ||
|
||
import org.hibernate.testing.orm.domain.StandardDomainModel; | ||
import org.hibernate.testing.orm.domain.contacts.Contact; | ||
import org.hibernate.testing.orm.jdbc.PreparedStatementSpyConnectionProvider; | ||
import org.hibernate.testing.orm.jdbc.PreparedStatementSpyConnectionProviderSettingProvider; | ||
import org.hibernate.testing.orm.junit.DialectFeatureChecks; | ||
import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; | ||
import org.hibernate.testing.orm.junit.Jpa; | ||
import org.hibernate.testing.orm.junit.RequiresDialectFeature; | ||
import org.hibernate.testing.orm.junit.SettingProvider; | ||
import org.junit.jupiter.api.BeforeAll; | ||
import org.junit.jupiter.api.Test; | ||
|
||
import jakarta.persistence.EntityManager; | ||
import jakarta.persistence.Query; | ||
|
||
@Jpa( | ||
standardModels = StandardDomainModel.CONTACTS, | ||
settingProviders = { | ||
@SettingProvider(settingName = AvailableSettings.CONNECTION_PROVIDER, | ||
provider = PreparedStatementSpyConnectionProviderSettingProvider.class) | ||
} | ||
) | ||
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJdbcDriverProxying.class) | ||
public class QuerySqlExceptionTest { | ||
|
||
private PreparedStatementSpyConnectionProvider connectionProvider; | ||
|
||
@BeforeAll | ||
public void init(EntityManagerFactoryScope scope) { | ||
connectionProvider = (PreparedStatementSpyConnectionProvider) scope.getEntityManagerFactory().getProperties() | ||
.get( AvailableSettings.CONNECTION_PROVIDER ); | ||
} | ||
|
||
@Test | ||
public void sqlExceptionOnExecutionWillCloseStatement(EntityManagerFactoryScope scope) { | ||
// We need at least one row in the "contacts" table, | ||
// otherwise the SELECT below might not even get executed completely | ||
// (the DB somehow detects the result will be 0 rows anyway and doesn't bother evaluating parameters). | ||
scope.inTransaction( entityManager -> { | ||
var contact = new Contact( | ||
1, | ||
new Contact.Name( "John", "Doe" ), | ||
Contact.Gender.MALE, | ||
LocalDate.of( 1970, 1, 1 ) | ||
); | ||
entityManager.persist( contact ); | ||
} ); | ||
scope.inTransaction( entityManager -> { | ||
connectionProvider.clear(); | ||
assertThatThrownBy( () -> createQueryTriggeringStatementExecutionFailure( entityManager ).getResultList() ) | ||
.satisfiesAnyOf( | ||
// Behavior differs depending on the dialect | ||
e -> assertThat( e ).isInstanceOf( SQLException.class ), | ||
e -> assertThat( e ).hasCauseInstanceOf( SQLException.class ), | ||
e -> assertThat( e ).hasRootCauseInstanceOf( SQLException.class ) | ||
); | ||
// Checking immediately, because the JDBC driver or connection pool might "fix" statement leaks | ||
// when the connection gets closed on transaction commit. | ||
assertThat( connectionProvider.getPreparedStatementsAndSql().entrySet() ) | ||
.isNotEmpty() | ||
.allSatisfy( e -> { | ||
try { | ||
assertThat( e.getKey().isClosed() ) | ||
.as( "Statement '" + e.getValue() + "' is closed" ) | ||
.isTrue(); | ||
} | ||
catch (SQLException ex) { | ||
throw new RuntimeException( ex ); | ||
} | ||
} ); | ||
} ); | ||
} | ||
|
||
// Creates a query that will intentionally trigger an exception | ||
// during statement execution (not during preparation). | ||
private Query createQueryTriggeringStatementExecutionFailure(EntityManager entityManager) { | ||
var dialect = entityManager.getEntityManagerFactory().unwrap( SessionFactoryImplementor.class ) | ||
.getJdbcServices().getDialect(); | ||
Object badParamValue; | ||
if ( dialect instanceof MySQLDialect ) { | ||
// These databases are perfectly fine with the operation `"foo" / 2` | ||
// and will happily return `0.0` without any error... | ||
// Let's give them something even more nonsensical | ||
// (but which we cannot pass to other DBs as they would detect the problem too early) | ||
badParamValue = List.of( "foo", "bar" ); | ||
} | ||
else { | ||
badParamValue = "foo"; | ||
} | ||
return entityManager.createNativeQuery( "select ( :param / 2 ) from contacts" ) | ||
.setParameter( "param", badParamValue ); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters