HHH-16875 be a bit more forgiving when type checking expressions involving unknown HQL functions
Let's not reject expressions like: function('current_user') = 'username' also add QueryArgumentException
This commit is contained in:
parent
a2defad7a4
commit
cd02a961c8
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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.query;
|
||||
|
||||
/**
|
||||
* An error that occurs binding an argument to a query parameter.
|
||||
* Usually indicates that the argument is of a type not assignable
|
||||
* to the type of the parameter.
|
||||
*
|
||||
* @since 6.3
|
||||
*
|
||||
* @author Gavin King
|
||||
*/
|
||||
public class QueryArgumentException extends IllegalArgumentException {
|
||||
private final Class<?> parameterType;
|
||||
private final Object argument;
|
||||
|
||||
public QueryArgumentException(String message, Class<?> parameterType, Object argument) {
|
||||
super(message);
|
||||
this.parameterType = parameterType;
|
||||
this.argument = argument;
|
||||
}
|
||||
|
||||
public Class<?> getParameterType() {
|
||||
return parameterType;
|
||||
}
|
||||
|
||||
public Object getArgument() {
|
||||
return argument;
|
||||
}
|
||||
}
|
|
@ -213,6 +213,9 @@ import org.hibernate.type.BasicType;
|
|||
import org.hibernate.type.descriptor.java.JavaType;
|
||||
import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType;
|
||||
|
||||
import org.hibernate.type.descriptor.java.spi.UnknownBasicJavaType;
|
||||
import org.hibernate.type.descriptor.jdbc.ObjectJdbcType;
|
||||
import org.hibernate.type.internal.BasicTypeImpl;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
|
@ -237,6 +240,7 @@ import static org.hibernate.grammars.hql.HqlParser.ListaggFunctionContext;
|
|||
import static org.hibernate.grammars.hql.HqlParser.OnOverflowClauseContext;
|
||||
import static org.hibernate.grammars.hql.HqlParser.PLUS;
|
||||
import static org.hibernate.grammars.hql.HqlParser.UNION;
|
||||
import static org.hibernate.internal.util.QuotingHelper.unquoteStringLiteral;
|
||||
import static org.hibernate.query.sqm.TemporalUnit.DATE;
|
||||
import static org.hibernate.query.sqm.TemporalUnit.DAY_OF_MONTH;
|
||||
import static org.hibernate.query.sqm.TemporalUnit.DAY_OF_WEEK;
|
||||
|
@ -2577,7 +2581,7 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
|
|||
else {
|
||||
assert child instanceof TerminalNode;
|
||||
final TerminalNode terminalNode = (TerminalNode) child;
|
||||
final String escape = QuotingHelper.unquoteStringLiteral( terminalNode.getText() );
|
||||
final String escape = unquoteStringLiteral( terminalNode.getText() );
|
||||
if ( escape.length() != 1 ) {
|
||||
throw new SemanticException(
|
||||
"Escape character literals must have exactly a single character, but found: " + escape
|
||||
|
@ -3450,7 +3454,7 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
|
|||
final TerminalNode firstChild = (TerminalNode) ctx.getChild( 0 );
|
||||
final String timezoneText;
|
||||
if ( firstChild.getSymbol().getType() == HqlParser.STRING_LITERAL ) {
|
||||
timezoneText = QuotingHelper.unquoteStringLiteral( ctx.getText() );
|
||||
timezoneText = unquoteStringLiteral( ctx.getText() );
|
||||
}
|
||||
else {
|
||||
timezoneText = ctx.getText();
|
||||
|
@ -3617,7 +3621,7 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
|
|||
|
||||
private SqmLiteral<String> stringLiteral(String text) {
|
||||
return new SqmLiteral<>(
|
||||
QuotingHelper.unquoteStringLiteral( text ),
|
||||
unquoteStringLiteral( text ),
|
||||
resolveExpressibleTypeBasic( String.class ),
|
||||
creationContext.getNodeBuilder()
|
||||
);
|
||||
|
@ -3880,11 +3884,11 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
|
|||
|
||||
@Override
|
||||
public SqmExpression<?> visitJpaNonstandardFunction(HqlParser.JpaNonstandardFunctionContext ctx) {
|
||||
final String functionName = QuotingHelper.unquoteStringLiteral( ctx.getChild( 2 ).getText() ).toLowerCase();
|
||||
final String functionName = unquoteStringLiteral( ctx.jpaNonstandardFunctionName().getText() ).toLowerCase();
|
||||
final List<SqmTypedNode<?>> functionArguments;
|
||||
if ( ctx.getChildCount() > 4 ) {
|
||||
//noinspection unchecked
|
||||
functionArguments = (List<SqmTypedNode<?>>) ctx.getChild( 4 ).accept( this );
|
||||
functionArguments = (List<SqmTypedNode<?>>) ctx.genericFunctionArguments().accept( this );
|
||||
}
|
||||
else {
|
||||
functionArguments = emptyList();
|
||||
|
@ -3897,7 +3901,10 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
|
|||
true,
|
||||
null,
|
||||
StandardFunctionReturnTypeResolvers.invariant(
|
||||
resolveExpressibleTypeBasic( Object.class )
|
||||
new BasicTypeImpl<>(
|
||||
new UnknownBasicJavaType<>( Object.class ),
|
||||
ObjectJdbcType.INSTANCE
|
||||
)
|
||||
),
|
||||
null
|
||||
);
|
||||
|
@ -4396,7 +4403,7 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
|
|||
|
||||
@Override
|
||||
public Object visitFormat(HqlParser.FormatContext ctx) {
|
||||
final String format = QuotingHelper.unquoteStringLiteral( ctx.getChild( 0 ).getText() );
|
||||
final String format = unquoteStringLiteral( ctx.getChild( 0 ).getText() );
|
||||
return new SqmFormat(
|
||||
format,
|
||||
resolveExpressibleTypeBasic( String.class ),
|
||||
|
@ -4898,7 +4905,7 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
|
|||
@Override
|
||||
public SqmLiteral<Character> visitTrimCharacter(HqlParser.TrimCharacterContext ctx) {
|
||||
final String trimCharText = ctx != null
|
||||
? QuotingHelper.unquoteStringLiteral( ctx.getText() )
|
||||
? unquoteStringLiteral( ctx.getText() )
|
||||
: " "; // JPA says space is the default
|
||||
|
||||
if ( trimCharText.length() != 1 ) {
|
||||
|
|
|
@ -12,6 +12,7 @@ import java.util.Date;
|
|||
|
||||
import org.hibernate.engine.spi.SessionFactoryImplementor;
|
||||
import org.hibernate.query.BindableType;
|
||||
import org.hibernate.query.QueryArgumentException;
|
||||
import org.hibernate.query.sqm.SqmExpressible;
|
||||
import org.hibernate.type.descriptor.java.JavaType;
|
||||
|
||||
|
@ -80,14 +81,16 @@ public class QueryParameterBindingValidator {
|
|||
bind,
|
||||
temporalPrecision
|
||||
) ) {
|
||||
throw new IllegalArgumentException(
|
||||
throw new QueryArgumentException(
|
||||
String.format(
|
||||
"Argument [%s] of type [%s] did not match parameter type [%s (%s)]",
|
||||
bind,
|
||||
bind.getClass().getName(),
|
||||
parameterJavaType.getName(),
|
||||
extractName( temporalPrecision )
|
||||
)
|
||||
),
|
||||
parameterJavaType,
|
||||
bind
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -104,13 +107,16 @@ public class QueryParameterBindingValidator {
|
|||
// validate the elements...
|
||||
for ( Object element : value ) {
|
||||
if ( !isValidBindValue( parameterType, element, temporalType ) ) {
|
||||
throw new IllegalArgumentException(
|
||||
throw new QueryArgumentException(
|
||||
String.format(
|
||||
"Parameter value element [%s] did not match expected type [%s (%s)]",
|
||||
element,
|
||||
parameterType.getName(),
|
||||
extractName( temporalType )
|
||||
)
|
||||
,
|
||||
parameterType,
|
||||
element
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -193,12 +199,14 @@ public class QueryParameterBindingValidator {
|
|||
Object value,
|
||||
TemporalType temporalType) {
|
||||
if ( !parameterType.isArray() ) {
|
||||
throw new IllegalArgumentException(
|
||||
throw new QueryArgumentException(
|
||||
String.format(
|
||||
"Encountered array-valued parameter binding, but was expecting [%s (%s)]",
|
||||
parameterType.getName(),
|
||||
extractName( temporalType )
|
||||
)
|
||||
),
|
||||
parameterType,
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -206,13 +214,15 @@ public class QueryParameterBindingValidator {
|
|||
// we have a primitive array. we validate that the actual array has the component type (type of elements)
|
||||
// we expect based on the component type of the parameter specification
|
||||
if ( !parameterType.getComponentType().isAssignableFrom( value.getClass().getComponentType() ) ) {
|
||||
throw new IllegalArgumentException(
|
||||
throw new QueryArgumentException(
|
||||
String.format(
|
||||
"Primitive array-valued parameter bind value type [%s] did not match expected type [%s (%s)]",
|
||||
value.getClass().getComponentType().getName(),
|
||||
parameterType.getName(),
|
||||
extractName( temporalType )
|
||||
)
|
||||
),
|
||||
parameterType,
|
||||
value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -222,13 +232,15 @@ public class QueryParameterBindingValidator {
|
|||
final Object[] array = (Object[]) value;
|
||||
for ( Object element : array ) {
|
||||
if ( !isValidBindValue( parameterType.getComponentType(), element, temporalType ) ) {
|
||||
throw new IllegalArgumentException(
|
||||
throw new QueryArgumentException(
|
||||
String.format(
|
||||
"Array-valued parameter value element [%s] did not match expected type [%s (%s)]",
|
||||
element,
|
||||
parameterType.getName(),
|
||||
extractName( temporalType )
|
||||
)
|
||||
),
|
||||
parameterType,
|
||||
array
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@ import org.hibernate.query.sqm.tree.expression.SqmLiteralNull;
|
|||
import org.hibernate.type.BasicType;
|
||||
import org.hibernate.type.descriptor.jdbc.JdbcType;
|
||||
|
||||
import static org.hibernate.type.descriptor.java.JavaTypeHelper.isUnknown;
|
||||
|
||||
/**
|
||||
* Functions for typechecking comparison expressions and assignments in the SQM tree.
|
||||
* A comparison expression is any predicate like {@code x = y} or {@code x > y}. An
|
||||
|
@ -285,7 +287,8 @@ public class TypecheckUtil {
|
|||
}
|
||||
|
||||
private static boolean isSameJavaType(SqmExpressible<?> leftType, SqmExpressible<?> rightType) {
|
||||
return leftType.getRelationalJavaType() == rightType.getRelationalJavaType()
|
||||
return isUnknown( leftType.getExpressibleJavaType() ) || isUnknown( rightType.getExpressibleJavaType() )
|
||||
|| leftType.getRelationalJavaType() == rightType.getRelationalJavaType()
|
||||
|| leftType.getExpressibleJavaType() == rightType.getExpressibleJavaType()
|
||||
|| leftType.getBindableJavaType() == rightType.getBindableJavaType();
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.hibernate.query.sqm.tree.expression.SqmExtractUnit;
|
|||
import org.hibernate.query.sqm.tree.expression.SqmTrimSpecification;
|
||||
import org.hibernate.sql.ast.tree.SqlAstNode;
|
||||
import org.hibernate.sql.ast.tree.expression.Expression;
|
||||
import org.hibernate.type.BasicType;
|
||||
import org.hibernate.type.JavaObjectType;
|
||||
import org.hibernate.type.descriptor.java.JavaType;
|
||||
import org.hibernate.type.descriptor.java.spi.JdbcTypeRecommendationException;
|
||||
|
@ -42,6 +43,7 @@ import static org.hibernate.type.SqlTypes.isIntegral;
|
|||
import static org.hibernate.type.SqlTypes.isNumericType;
|
||||
import static org.hibernate.type.SqlTypes.isSpatialType;
|
||||
import static org.hibernate.type.SqlTypes.isTemporalType;
|
||||
import static org.hibernate.type.descriptor.java.JavaTypeHelper.isUnknown;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -132,25 +134,27 @@ public class ArgumentTypesValidator implements ArgumentsValidator {
|
|||
JdbcTypeIndicators indicators,
|
||||
FunctionParameterType type,
|
||||
JavaType<?> javaType) {
|
||||
DomainType<?> domainType = argument.getExpressible().getSqmType();
|
||||
if ( domainType instanceof JdbcMapping ) {
|
||||
checkArgumentType(
|
||||
count, functionName, type,
|
||||
((JdbcMapping) domainType).getJdbcType().getDefaultSqlTypeCode(),
|
||||
javaType.getJavaTypeClass()
|
||||
);
|
||||
}
|
||||
else {
|
||||
//TODO: this branch is now probably obsolete and can be deleted!
|
||||
try {
|
||||
if ( !isUnknown( javaType ) ) {
|
||||
DomainType<?> domainType = argument.getExpressible().getSqmType();
|
||||
if ( domainType instanceof JdbcMapping ) {
|
||||
checkArgumentType(
|
||||
count, functionName, type,
|
||||
getJdbcType( indicators, javaType ),
|
||||
((JdbcMapping) domainType).getJdbcType().getDefaultSqlTypeCode(),
|
||||
javaType.getJavaTypeClass()
|
||||
);
|
||||
}
|
||||
catch (JdbcTypeRecommendationException e) {
|
||||
// it's a converter or something like that, and we will check it later
|
||||
else {
|
||||
//TODO: this branch is now probably obsolete and can be deleted!
|
||||
try {
|
||||
checkArgumentType(
|
||||
count, functionName, type,
|
||||
getJdbcType( indicators, javaType ),
|
||||
javaType.getJavaTypeClass()
|
||||
);
|
||||
}
|
||||
catch (JdbcTypeRecommendationException e) {
|
||||
// it's a converter or something like that, and we will check it later
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -177,21 +181,31 @@ public class ArgumentTypesValidator implements ArgumentsValidator {
|
|||
@Override
|
||||
public void validateSqlTypes(List<? extends SqlAstNode> arguments, String functionName) {
|
||||
int count = 0;
|
||||
for (SqlAstNode argument : arguments) {
|
||||
if (argument instanceof Expression) {
|
||||
JdbcMappingContainer expressionType = ((Expression) argument).getExpressionType();
|
||||
for ( SqlAstNode argument : arguments ) {
|
||||
if ( argument instanceof Expression ) {
|
||||
final Expression expression = (Expression) argument;
|
||||
final JdbcMappingContainer expressionType = expression.getExpressionType();
|
||||
if (expressionType != null) {
|
||||
if (expressionType instanceof JavaObjectType) {
|
||||
if ( isUnknownExpressionType( expressionType ) ) {
|
||||
count += expressionType.getJdbcTypeCount();
|
||||
}
|
||||
else {
|
||||
count = validateArgument(count, expressionType, functionName);
|
||||
count = validateArgument( count, expressionType, functionName );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We can't validate some expressions involving parameters / unknown functions.
|
||||
*/
|
||||
private static boolean isUnknownExpressionType(JdbcMappingContainer expressionType) {
|
||||
return expressionType instanceof JavaObjectType
|
||||
|| expressionType instanceof BasicType
|
||||
&& isUnknown( ((BasicType<?>) expressionType).getJavaTypeDescriptor() );
|
||||
}
|
||||
|
||||
private int validateArgument(int count, JdbcMappingContainer expressionType, String functionName) {
|
||||
final int jdbcTypeCount = expressionType.getJdbcTypeCount();
|
||||
for ( int i = 0; i < jdbcTypeCount; i++ ) {
|
||||
|
@ -275,7 +289,7 @@ public class ArgumentTypesValidator implements ArgumentsValidator {
|
|||
private void throwError(FunctionParameterType type, Type javaType, String functionName, int count) {
|
||||
throw new FunctionArgumentException(
|
||||
String.format(
|
||||
"Parameter %d of function %s() has type %s, but argument is of type %s",
|
||||
"Parameter %d of function '%s()' has type '%s', but argument is of type '%s'",
|
||||
count,
|
||||
functionName,
|
||||
type,
|
||||
|
|
|
@ -5349,10 +5349,10 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
|
|||
);
|
||||
}
|
||||
else {
|
||||
throw new SqlTreeCreationException(
|
||||
throw new SemanticException(
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"QueryLiteral type [`%s`] did not match domain Java-type [`%s`] nor JDBC Java-type [`%s`]",
|
||||
"Literal type '%s' did not match domain type '%s' nor converted type '%s'",
|
||||
value.getClass(),
|
||||
valueConverter.getDomainJavaType().getJavaTypeClass().getName(),
|
||||
valueConverter.getRelationalJavaType().getJavaTypeClass().getName()
|
||||
|
|
|
@ -14,8 +14,6 @@ import java.util.function.Supplier;
|
|||
import org.hibernate.annotations.Immutable;
|
||||
import org.hibernate.annotations.Mutability;
|
||||
import org.hibernate.internal.util.ReflectHelper;
|
||||
import org.hibernate.resource.beans.spi.ManagedBean;
|
||||
import org.hibernate.resource.beans.spi.ManagedBeanRegistry;
|
||||
import org.hibernate.type.descriptor.java.EnumJavaType;
|
||||
import org.hibernate.type.descriptor.java.ImmutableMutabilityPlan;
|
||||
import org.hibernate.type.descriptor.java.JavaType;
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
package org.hibernate.orm.test.jpa.criteria;
|
||||
|
||||
import org.hibernate.query.SemanticException;
|
||||
import org.hibernate.query.QueryArgumentException;
|
||||
import org.hibernate.testing.TestForIssue;
|
||||
import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
|
||||
import org.hibernate.testing.orm.junit.FailureExpected;
|
||||
import org.hibernate.testing.orm.junit.Jpa;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -39,7 +38,7 @@ public class ObjectParameterTypeForEmbeddableTest {
|
|||
);
|
||||
}
|
||||
|
||||
@Test @FailureExpected(reason = "This query is in my opinion not well-typed, and should be rejected")
|
||||
@Test
|
||||
public void testSettingParameterOfTypeObject(EntityManagerFactoryScope scope) {
|
||||
scope.inTransaction(
|
||||
entityManager -> {
|
||||
|
@ -77,8 +76,8 @@ public class ObjectParameterTypeForEmbeddableTest {
|
|||
|
||||
@Test
|
||||
public void testSettingParameterOfTypeWrongType(EntityManagerFactoryScope scope) {
|
||||
SemanticException thrown = assertThrows(
|
||||
SemanticException.class, () ->
|
||||
QueryArgumentException thrown = assertThrows(
|
||||
QueryArgumentException.class, () ->
|
||||
scope.inTransaction(
|
||||
entityManager -> {
|
||||
final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
|
||||
|
@ -94,7 +93,7 @@ public class ObjectParameterTypeForEmbeddableTest {
|
|||
)
|
||||
);
|
||||
|
||||
assertThat( thrown.getMessage() ).startsWith( "Cannot compare left expression" );
|
||||
assertThat( thrown.getMessage() ).contains( "did not match parameter type" );
|
||||
}
|
||||
|
||||
@Entity(name = "TestEntity")
|
||||
|
|
|
@ -21,7 +21,6 @@ import org.hibernate.engine.spi.SessionImplementor;
|
|||
import org.hibernate.internal.util.ExceptionHelper;
|
||||
import org.hibernate.query.Query;
|
||||
import org.hibernate.query.SemanticException;
|
||||
import org.hibernate.sql.ast.SqlTreeCreationException;
|
||||
|
||||
import org.hibernate.testing.orm.junit.DomainModel;
|
||||
import org.hibernate.testing.orm.junit.SessionFactory;
|
||||
|
@ -29,9 +28,7 @@ import org.hibernate.testing.orm.junit.SessionFactoryScope;
|
|||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
|
Loading…
Reference in New Issue