HHH-16917 - Number not allowed as type for procedure query parameter

This commit is contained in:
Steve Ebersole 2023-07-11 08:55:54 -05:00
parent 8031952d86
commit 8386e1851e
5 changed files with 243 additions and 23 deletions

View File

@ -0,0 +1,20 @@
/*
* 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.procedure;
import org.hibernate.HibernateException;
/**
* Indicates Hibernate is unable to determine the type details for a parameter.
*
* @author Steve Ebersole
*/
public class ParameterTypeException extends HibernateException {
public ParameterTypeException(String message) {
super( message );
}
}

View File

@ -9,10 +9,12 @@ package org.hibernate.procedure.internal;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Locale;
import java.util.Objects;
import org.hibernate.engine.jdbc.env.spi.ExtractedDatabaseMetaData;
import org.hibernate.metamodel.mapping.JdbcMapping;
import org.hibernate.procedure.ParameterTypeException;
import org.hibernate.procedure.spi.NamedCallableQueryMemento;
import org.hibernate.procedure.spi.ParameterStrategy;
import org.hibernate.procedure.spi.ProcedureCallImplementor;
@ -44,6 +46,9 @@ public class ProcedureParameterImpl<T> extends AbstractQueryParameter<T> impleme
private final ParameterMode mode;
private final Class<T> javaType;
/**
* Used for named Query parameters
*/
public ProcedureParameterImpl(
String name,
ParameterMode mode,
@ -56,6 +61,9 @@ public class ProcedureParameterImpl<T> extends AbstractQueryParameter<T> impleme
this.javaType = javaType;
}
/**
* Used for ordinal Query parameters
*/
public ProcedureParameterImpl(
Integer position,
ParameterMode mode,
@ -115,21 +123,32 @@ public class ProcedureParameterImpl<T> extends AbstractQueryParameter<T> impleme
int startIndex,
ProcedureCallImplementor<?> procedureCall) {
final QueryParameterBinding<T> binding = procedureCall.getParameterBindings().getBinding( this );
final boolean isNamed = procedureCall.getParameterStrategy() == ParameterStrategy.NAMED && this.name != null;
final BindableType<T> bindableType;
if ( getHibernateType() != null ) {
bindableType = getHibernateType();
}
else if ( binding != null ) {
//noinspection unchecked
bindableType = (BindableType<T>) binding.getBindType();
}
else {
bindableType = null;
}
final OutputableType<T> typeToUse = (OutputableType<T>) BindingTypeHelper.INSTANCE.resolveTemporalPrecision(
binding == null || binding.getExplicitTemporalPrecision() == null
? null
: binding.getExplicitTemporalPrecision(),
getHibernateType(),
binding == null ? null : binding.getExplicitTemporalPrecision(),
bindableType,
procedureCall.getSession().getFactory()
);
final String name;
if ( procedureCall.getParameterStrategy() == ParameterStrategy.NAMED
&& canDoNameParameterBinding( typeToUse, procedureCall ) ) {
name = this.name;
final String jdbcParamName;
if ( isNamed && canDoNameParameterBinding( typeToUse, procedureCall ) ) {
jdbcParamName = this.name;
}
else {
name = null;
jdbcParamName = null;
}
final JdbcParameterBinder parameterBinder;
@ -138,31 +157,58 @@ public class ProcedureParameterImpl<T> extends AbstractQueryParameter<T> impleme
switch ( mode ) {
case REF_CURSOR:
refCursorExtractor = new JdbcCallRefCursorExtractorImpl( name, startIndex );
refCursorExtractor = new JdbcCallRefCursorExtractorImpl( jdbcParamName, startIndex );
parameterBinder = null;
parameterExtractor = null;
break;
case IN:
parameterBinder = getParameterBinder( typeToUse, name );
validateBindableType( typeToUse, startIndex );
parameterBinder = getParameterBinder( typeToUse, jdbcParamName );
parameterExtractor = null;
refCursorExtractor = null;
break;
case INOUT:
parameterBinder = getParameterBinder( typeToUse, name );
parameterExtractor = new JdbcCallParameterExtractorImpl<>( procedureCall.getProcedureName(), name, startIndex, typeToUse );
validateBindableType( typeToUse, startIndex );
parameterBinder = getParameterBinder( typeToUse, jdbcParamName );
parameterExtractor = new JdbcCallParameterExtractorImpl<>( procedureCall.getProcedureName(), jdbcParamName, startIndex, typeToUse );
refCursorExtractor = null;
break;
default:
validateBindableType( typeToUse, startIndex );
parameterBinder = null;
parameterExtractor = new JdbcCallParameterExtractorImpl<>( procedureCall.getProcedureName(), name, startIndex, typeToUse );
parameterExtractor = new JdbcCallParameterExtractorImpl<>( procedureCall.getProcedureName(), jdbcParamName, startIndex, typeToUse );
refCursorExtractor = null;
break;
}
return new JdbcCallParameterRegistrationImpl( name, startIndex, mode, typeToUse, parameterBinder, parameterExtractor, refCursorExtractor );
return new JdbcCallParameterRegistrationImpl( jdbcParamName, startIndex, mode, typeToUse, parameterBinder, parameterExtractor, refCursorExtractor );
}
private void validateBindableType(BindableType<T> bindableType, int startIndex) {
if ( bindableType == null ) {
throw new ParameterTypeException(
String.format(
Locale.ROOT,
"Could not determine ProcedureCall parameter bind type - %s (%s)",
this.name != null ? this.name : this.position,
startIndex
)
);
}
}
private JdbcParameterBinder getParameterBinder(BindableType<T> typeToUse, String name) {
if ( typeToUse == null ) {
throw new ParameterTypeException(
String.format(
Locale.ROOT,
"Cannot determine the bindable type for procedure parameter %s (%s)",
this.name != null ? this.name : this.position,
name
)
);
}
if ( typeToUse instanceof BasicType<?> ) {
if ( name == null ) {
return new JdbcParameterImpl( (BasicType<T>) typeToUse );
@ -190,12 +236,8 @@ public class ProcedureParameterImpl<T> extends AbstractQueryParameter<T> impleme
};
}
}
else if ( typeToUse == null ) {
throw new IllegalArgumentException( "Cannot determine the bindable type for procedure parameter: " + name );
}
else {
throw new UnsupportedOperationException();
}
throw new UnsupportedOperationException();
}
private boolean canDoNameParameterBinding(

View File

@ -11,8 +11,6 @@ import java.sql.SQLException;
import org.hibernate.engine.jdbc.cursor.spi.RefCursorSupport;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.query.BindableType;
import org.hibernate.metamodel.model.domain.BasicDomainType;
import org.hibernate.query.OutputableType;
import org.hibernate.sql.exec.spi.JdbcCallParameterExtractor;
import org.hibernate.sql.exec.spi.JdbcCallParameterRegistration;

View File

@ -0,0 +1,42 @@
/*
* 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.procedure;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.function.Consumer;
import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess;
import org.hibernate.engine.jdbc.spi.JdbcServices;
import org.hibernate.engine.spi.SessionFactoryImplementor;
/**
* @author Steve Ebersole
*/
public class Helper {
@FunctionalInterface
public interface StatementAction {
void accept(Statement statement) throws SQLException;
}
public static void withStatement(SessionFactoryImplementor sessionFactory, StatementAction action) throws SQLException {
final JdbcConnectionAccess connectionAccess = sessionFactory.getServiceRegistry()
.getService( JdbcServices.class )
.getBootstrapJdbcConnectionAccess();
try (Connection connection = connectionAccess.obtainConnection()) {
withStatement( connection, action );
}
}
public static void withStatement(Connection connection, StatementAction action) throws SQLException {
try (Statement statement = connection.createStatement()) {
action.accept( statement );
}
}
}

View File

@ -0,0 +1,118 @@
/*
* 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.procedure;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.SQLException;
import org.hibernate.dialect.HSQLDialect;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.procedure.ParameterTypeException;
import org.hibernate.procedure.ProcedureCall;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.Jira;
import org.hibernate.testing.orm.junit.RequiresDialect;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import jakarta.persistence.ParameterMode;
import jakarta.persistence.StoredProcedureQuery;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hibernate.type.descriptor.java.CoercionHelper.toShort;
import static org.junit.jupiter.api.Assertions.fail;
/**
* Test for using unsupported Java type ({@link Number} e.g.) as a parameter type
* for {@link ProcedureCall} / {@link StoredProcedureQuery} query parameters
*
* @author Steve Ebersole
*/
@RequiresDialect( HSQLDialect.class )
@DomainModel
@SessionFactory
@Jira( "https://hibernate.atlassian.net/browse/HHH-16917" )
public class NumericParameterTypeTests {
@Test
void testParameterBaseline(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
executeQuery( Integer.class, Integer.class, 1, 2, session );
} );
}
private void executeQuery(
Class<? extends Number> inArgType,
Class<? extends Number> outArgType,
Object inArgValue,
Object expectedOutArgValue,
SessionImplementor session) {
final StoredProcedureQuery query = session.createStoredProcedureQuery( "inoutproc" );
query.registerStoredProcedureParameter( "inarg", inArgType, ParameterMode.IN );
query.registerStoredProcedureParameter( "outarg", outArgType, ParameterMode.OUT );
query.setParameter( "inarg", inArgValue );
final Object result = query.getOutputParameterValue( "outarg" );
assertThat( result ).isEqualTo( expectedOutArgValue );
}
@Test
void testInputParameter(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
// Number is fine for IN parameters because we can ultimately look at the bind value type
executeQuery( Number.class, Integer.class, 1, 2, session );
// in addition to Integer/Integer, we can also have IN parameters defined as numerous "implicit conversion" types
executeQuery( Short.class, Integer.class, 1, 2, session );
executeQuery( BigInteger.class, Integer.class, BigInteger.ONE, 2, session );
executeQuery( Double.class, Integer.class, 1.0, 2, session );
executeQuery( BigDecimal.class, Integer.class, BigDecimal.ONE, 2, session );
} );
}
@Test
void testOutputParameter(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
try {
// Number is not fine for OUT parameters
executeQuery( Integer.class, Number.class, 1, 2, session );
fail( "Expected a ParameterTypeException" );
}
catch (ParameterTypeException expected) {
assertThat( expected.getMessage() ).contains( "outarg" );
}
// in addition to Integer/Integer, we can also have OUT parameters defined as numerous "implicit conversion" types
executeQuery( Integer.class, Short.class, 1, toShort( 2 ), session );
executeQuery( Integer.class, BigInteger.class, BigDecimal.ONE, BigInteger.valueOf( 2 ), session );
executeQuery( Integer.class, Double.class, 1, 2.0, session );
executeQuery( Integer.class, BigDecimal.class, 1, BigDecimal.valueOf( 2 ), session );
} );
}
@BeforeAll
void createProcedures(SessionFactoryScope scope) throws SQLException {
final String procedureStatement = "create procedure inoutproc (IN inarg numeric, OUT outarg numeric) " +
"begin atomic set outarg = inarg + 1;" +
"end";
Helper.withStatement( scope.getSessionFactory(), statement -> {
statement.execute( procedureStatement );
} );
}
@AfterAll
void dropProcedures(SessionFactoryScope scope) throws SQLException {
Helper.withStatement( scope.getSessionFactory(), statement -> {
statement.execute( "drop procedure inoutproc" );
} );
}
}