From 2955e0b91d78b4879e62d6957d27d49bcb2f8a4c Mon Sep 17 00:00:00 2001 From: Gavin King Date: Wed, 27 Mar 2024 22:40:40 +0100 Subject: [PATCH] spec-compliant inference of entity type in @Query Signed-off-by: Gavin King --- .../hibernate/internal/util/StringHelper.java | 6 +- .../processor/test/data/eg/Bookshop.java | 4 + .../annotation/AnnotationMetaEntity.java | 73 +++++++++++++++++-- 3 files changed, 72 insertions(+), 11 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/StringHelper.java b/hibernate-core/src/main/java/org/hibernate/internal/util/StringHelper.java index 0999808f3d..8271ddd751 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/StringHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/StringHelper.java @@ -561,15 +561,15 @@ public final class StringHelper { return count; } - public static boolean isNotEmpty(String string) { + public static boolean isNotEmpty(@Nullable String string) { return string != null && !string.isEmpty(); } - public static boolean isEmpty(String string) { + public static boolean isEmpty(@Nullable String string) { return string == null || string.isEmpty(); } - public static boolean isBlank(String string) { + public static boolean isBlank(@Nullable String string) { return string == null || string.isBlank(); } diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/eg/Bookshop.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/eg/Bookshop.java index d3e623a866..67ce6bc358 100644 --- a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/eg/Bookshop.java +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/eg/Bookshop.java @@ -2,6 +2,7 @@ package org.hibernate.processor.test.data.eg; import jakarta.data.repository.CrudRepository; import jakarta.data.repository.Find; +import jakarta.data.repository.Query; import jakarta.data.repository.Repository; import jakarta.transaction.Transactional; @@ -12,4 +13,7 @@ public interface Bookshop extends CrudRepository { @Find @Transactional List byPublisher(String publisher_name); + + @Query("select isbn where title like ?1 order by isbn") + String[] ssns(String title); } diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java index c8edc448fe..8aadec5825 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java @@ -6,8 +6,10 @@ */ package org.hibernate.processor.annotation; +import org.antlr.v4.runtime.Token; import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.AssertionFailure; +import org.hibernate.grammars.hql.HqlLexer; import org.hibernate.processor.Context; import org.hibernate.processor.ImportContextImpl; import org.hibernate.processor.ProcessLaterException; @@ -23,6 +25,7 @@ import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.query.criteria.JpaEntityJoin; import org.hibernate.query.criteria.JpaRoot; import org.hibernate.query.criteria.JpaSelection; +import org.hibernate.query.hql.internal.HqlParseTreeBuilder; import org.hibernate.query.sql.internal.ParameterParser; import org.hibernate.query.sql.spi.ParameterRecognizer; import org.hibernate.query.sqm.SqmExpressible; @@ -66,6 +69,12 @@ import static java.util.Collections.emptyList; import static java.util.stream.Collectors.toList; import static javax.lang.model.util.ElementFilter.fieldsIn; import static javax.lang.model.util.ElementFilter.methodsIn; +import static org.hibernate.grammars.hql.HqlLexer.FROM; +import static org.hibernate.grammars.hql.HqlLexer.GROUP; +import static org.hibernate.grammars.hql.HqlLexer.HAVING; +import static org.hibernate.grammars.hql.HqlLexer.ORDER; +import static org.hibernate.grammars.hql.HqlLexer.WHERE; +import static org.hibernate.internal.util.StringHelper.isNotEmpty; import static org.hibernate.internal.util.StringHelper.qualify; import static org.hibernate.processor.annotation.AbstractQueryMethod.isSessionParameter; import static org.hibernate.processor.annotation.AbstractQueryMethod.isSpecialParam; @@ -2077,13 +2086,6 @@ public class AnnotationMetaEntity extends AnnotationMeta { final List paramNames = parameterNames( method ); final List paramTypes = parameterTypes( method ); - if ( isNative ) { - validateSql( method, mirror, queryString, paramNames, value ); - } - else { - validateHql( method, returnType, mirror, value, queryString, paramNames, paramTypes ); - } - // now check that the query has a parameter for every method parameter checkParameters( method, returnType, paramNames, paramTypes, mirror, value, queryString ); @@ -2093,11 +2095,21 @@ public class AnnotationMetaEntity extends AnnotationMeta { ? emptyList() : orderByList( method, (TypeElement) resultType.asElement() ); + final String processedQuery; + if ( isNative ) { + processedQuery = queryString; + validateSql( method, mirror, processedQuery, paramNames, value ); + } + else { + processedQuery = addFromClauseIfNecessary( queryString, implicitEntityName(resultType) ); + validateHql( method, returnType, mirror, value, processedQuery, paramNames, paramTypes ); + } + final QueryMethod attribute = new QueryMethod( this, method, method.getSimpleName().toString(), - queryString, + processedQuery, returnType == null ? null : returnType.toString(), containerTypeName, paramNames, @@ -2116,6 +2128,51 @@ public class AnnotationMetaEntity extends AnnotationMeta { } } + private @Nullable String implicitEntityName(@Nullable DeclaredType resultType) { + if ( resultType != null && hasAnnotation(resultType.asElement(), ENTITY) ) { + final AnnotationMirror annotation = + getAnnotationMirror(resultType.asElement(), ENTITY); + if ( annotation == null ) { + throw new AssertionFailure("@Entity annotation should not be missing"); + } + final String name = (String) getAnnotationValue(annotation, "name"); + return isNotEmpty(name) ? resultType.asElement().getSimpleName().toString() : name; + } + else if ( primaryEntity != null ) { + return primaryEntity.getSimpleName().toString(); + } + else { + return null; + } + } + + private static String addFromClauseIfNecessary(String hql, @Nullable String entityType) { + if ( entityType == null ) { + return hql; + } + else if ( isInsertUpdateDelete(hql) ) { + return hql; + } + else { + final HqlLexer hqlLexer = HqlParseTreeBuilder.INSTANCE.buildHqlLexer( hql ); + final List allTokens = hqlLexer.getAllTokens(); + for (Token token : allTokens) { + switch ( token.getType() ) { + case FROM: + return hql; + case WHERE: + case HAVING: + case GROUP: + case ORDER: + return new StringBuilder(hql) + .insert(token.getStartIndex(), "from " + entityType + " ") + .toString(); + } + } + return hql + " from " + entityType; + } + } + private @Nullable DeclaredType resultType( ExecutableElement method, @Nullable TypeMirror returnType, AnnotationMirror mirror, AnnotationValue value) { if ( returnType != null && returnType.getKind() == TypeKind.DECLARED ) {