From 8b5cdba5bcb95763c50066adee59ef46cb894825 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Mon, 13 May 2024 10:24:22 +0200 Subject: [PATCH] HHH-18089 Support bracket syntax with string types --- .../hql/internal/SemanticQueryBuilder.java | 51 ++++++++++++--- .../StandardFunctionReturnTypeResolvers.java | 45 ++++++-------- .../sqm/sql/BaseSqmToSqlAstConverter.java | 30 +++++---- .../tree/domain/SqmBasicValuedSimplePath.java | 33 +++++++--- .../sqm/tree/domain/SqmFunctionPath.java | 40 +++++++++--- .../orm/test/hql/StringBracketSyntaxTest.java | 62 +++++++++++++++++++ release-announcement.adoc | 9 +++ 7 files changed, 204 insertions(+), 66 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/hql/StringBracketSyntaxTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index d21379b2316d..bce1799bf677 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -5333,15 +5333,48 @@ else if ( ctx.simplePath() != null && ctx.indexedPathAccessFragment() != null ) } else if ( ctx.simplePath() != null && ctx.slicedPathAccessFragment() != null ) { final List slicedFragments = ctx.slicedPathAccessFragment().expression(); - return getFunctionDescriptor( "array_slice" ).generateSqmExpression( - List.of( - (SqmTypedNode) visitSimplePath( ctx.simplePath() ), - (SqmTypedNode) slicedFragments.get( 0 ).accept( this ), - (SqmTypedNode) slicedFragments.get( 1 ).accept( this ) - ), - null, - creationContext.getQueryEngine() - ); + final SqmTypedNode lhs = (SqmTypedNode) visitSimplePath( ctx.simplePath() ); + final SqmExpressible lhsExpressible = lhs.getExpressible(); + if ( lhsExpressible != null && lhsExpressible.getSqmType() instanceof BasicPluralType ) { + return getFunctionDescriptor( "array_slice" ).generateSqmExpression( + List.of( + lhs, + (SqmTypedNode) slicedFragments.get( 0 ).accept( this ), + (SqmTypedNode) slicedFragments.get( 1 ).accept( this ) + ), + null, + creationContext.getQueryEngine() + ); + } + else { + final SqmExpression start = (SqmExpression) slicedFragments.get( 0 ).accept( this ); + final SqmExpression end = (SqmExpression) slicedFragments.get( 1 ).accept( this ); + return getFunctionDescriptor( "substring" ).generateSqmExpression( + List.of( + lhs, + start, + new SqmBinaryArithmetic<>( + BinaryArithmeticOperator.ADD, + new SqmBinaryArithmetic<>( + BinaryArithmeticOperator.SUBTRACT, + end, + start, + creationContext.getJpaMetamodel(), + creationContext.getNodeBuilder() + ), + new SqmLiteral<>( + 1, + creationContext.getNodeBuilder().getIntegerType(), + creationContext.getNodeBuilder() + ), + creationContext.getJpaMetamodel(), + creationContext.getNodeBuilder() + ) + ), + null, + creationContext.getQueryEngine() + ); + } } else { throw new ParsingException( "Illegal domain path '" + ctx.getText() + "'" ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/StandardFunctionReturnTypeResolvers.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/StandardFunctionReturnTypeResolvers.java index fc206e14a00b..ec941199417f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/StandardFunctionReturnTypeResolvers.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/StandardFunctionReturnTypeResolvers.java @@ -25,10 +25,14 @@ import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.type.BasicType; +import org.hibernate.type.SqlTypes; import org.hibernate.type.spi.TypeConfiguration; import org.checkerframework.checker.nullness.qual.Nullable; +import static org.hibernate.type.SqlTypes.isCharacterOrClobType; +import static org.hibernate.type.SqlTypes.isNumericType; + /** * @author Steve Ebersole */ @@ -142,8 +146,7 @@ public ReturnableType resolveFunctionReturnType( // Internal helpers @Internal - public static boolean isAssignableTo( - ReturnableType defined, ReturnableType implied) { + public static boolean isAssignableTo(ReturnableType defined, ReturnableType implied) { if ( implied == null ) { return false; } @@ -152,19 +155,27 @@ public static boolean isAssignableTo( return true; } - if (!(implied instanceof BasicType) || !(defined instanceof BasicType) ) { + if ( !( implied instanceof BasicType ) || !( defined instanceof BasicType ) ) { return false; } + return isAssignableTo( + ( (BasicType) defined ).getJdbcMapping(), + ( (BasicType) implied ).getJdbcMapping() + ); + } + @Internal + public static boolean isAssignableTo(JdbcMapping defined, JdbcMapping implied) { //This list of cases defines legal promotions from a SQL function return //type specified in the function template (i.e. in the Dialect) and a type //that is determined by how the function is used in the HQL query. In essence //the types are compatible if the map to the same JDBC type, of if they are //both numeric types. - int impliedTypeCode = ((BasicType) implied).getJdbcMapping().getJdbcType().getDefaultSqlTypeCode(); - int definedTypeCode = ((BasicType) defined).getJdbcMapping().getJdbcType().getDefaultSqlTypeCode(); + int impliedTypeCode = implied.getJdbcType().getDefaultSqlTypeCode(); + int definedTypeCode = defined.getJdbcType().getDefaultSqlTypeCode(); return impliedTypeCode == definedTypeCode - || isNumeric( impliedTypeCode ) && isNumeric( definedTypeCode ); + || isNumericType( impliedTypeCode ) && isNumericType( definedTypeCode ) + || isCharacterOrClobType( impliedTypeCode ) && isCharacterOrClobType( definedTypeCode ); } @Internal @@ -202,27 +213,7 @@ private static boolean areCompatible( //that is determined by how the function is used in the HQL query. In essence //the types are compatible if the map to the same JDBC type, of if they are //both numeric types. - int impliedTypeCode = implied.getJdbcMapping().getJdbcType().getDefaultSqlTypeCode(); - int definedTypeCode = defined.getJdbcMapping().getJdbcType().getDefaultSqlTypeCode(); - return impliedTypeCode == definedTypeCode - || isNumeric( impliedTypeCode ) && isNumeric( definedTypeCode ); - - } - - private static boolean isNumeric(int type) { - switch ( type ) { - case Types.SMALLINT: - case Types.TINYINT: - case Types.INTEGER: - case Types.BIGINT: - case Types.FLOAT: - case Types.REAL: - case Types.DOUBLE: - case Types.NUMERIC: - case Types.DECIMAL: - return true; - } - return false; + return isAssignableTo( defined.getJdbcMapping(), implied.getJdbcMapping() ); } public static ReturnableType extractArgumentType( diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java index b4962ec26024..21168b9d8ac4 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java @@ -3694,7 +3694,10 @@ private TableGroup prepareReusablePath( } if ( parentPath == null ) { if ( sqmPath instanceof SqmFunctionPath ) { - return visitFunctionPath( (SqmFunctionPath) sqmPath ); + final SqmFunctionPath functionPath = (SqmFunctionPath) sqmPath; + if ( functionPath.getReferencedPathSource() instanceof CompositeSqmPathSource ) { + return (TableGroup) visitFunctionPath( functionPath ); + } } return null; } @@ -3794,7 +3797,7 @@ private void prepareForSelection(SqmPath selectionPath) { if ( tableGroup == null ) { prepareReusablePath( path, () -> null ); - if ( !( path instanceof SqmEntityValuedSimplePath + if ( path.getLhs() != null && !( path instanceof SqmEntityValuedSimplePath || path instanceof SqmEmbeddedValuedSimplePath || path instanceof SqmAnyValuedSimplePath || path instanceof SqmTreatedPath ) ) { @@ -4537,20 +4540,25 @@ public Expression visitIndexAggregateFunction(SqmIndexAggregateFunction path) } @Override - public TableGroup visitFunctionPath(SqmFunctionPath functionPath) { + public Expression visitFunctionPath(SqmFunctionPath functionPath) { final NavigablePath navigablePath = functionPath.getNavigablePath(); TableGroup tableGroup = getFromClauseAccess().findTableGroup( navigablePath ); if ( tableGroup == null ) { final Expression functionExpression = (Expression) functionPath.getFunction().accept( this ); - final EmbeddableMappingType embeddableMappingType = ( (AggregateJdbcType) functionExpression.getExpressionType() + final JdbcType jdbcType = functionExpression.getExpressionType() .getSingleJdbcMapping() - .getJdbcType() ).getEmbeddableMappingType(); - tableGroup = new EmbeddableFunctionTableGroup( - navigablePath, - embeddableMappingType, - functionExpression - ); - getFromClauseAccess().registerTableGroup( navigablePath, tableGroup ); + .getJdbcType(); + if ( jdbcType instanceof AggregateJdbcType ) { + tableGroup = new EmbeddableFunctionTableGroup( + navigablePath, + ( (AggregateJdbcType) jdbcType ).getEmbeddableMappingType(), + functionExpression + ); + getFromClauseAccess().registerTableGroup( navigablePath, tableGroup ); + } + else { + return functionExpression; + } } return tableGroup; } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmBasicValuedSimplePath.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmBasicValuedSimplePath.java index d5ee7f90bd4d..1b51657233d7 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmBasicValuedSimplePath.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmBasicValuedSimplePath.java @@ -110,17 +110,30 @@ public SqmPath resolveIndexedAccess( if ( indexedPath != null ) { return indexedPath; } - if ( !( getNodeType().getSqmPathType() instanceof BasicPluralType ) ) { - throw new UnsupportedOperationException( "Index access is only supported for basic plural types." ); - } + final DomainType sqmPathType = getNodeType().getSqmPathType(); final QueryEngine queryEngine = creationState.getCreationContext().getQueryEngine(); - final SelfRenderingSqmFunction result = queryEngine.getSqmFunctionRegistry() - .findFunctionDescriptor( "array_get" ) - .generateSqmExpression( - asList( this, selector ), - null, - queryEngine - ); + final SelfRenderingSqmFunction result; + if ( sqmPathType instanceof BasicPluralType ) { + result = queryEngine.getSqmFunctionRegistry() + .findFunctionDescriptor( "array_get" ) + .generateSqmExpression( + asList( this, selector ), + null, + queryEngine + ); + } + else if ( sqmPathType.getRelationalJavaType().getJavaTypeClass() == String.class ) { + result = queryEngine.getSqmFunctionRegistry() + .findFunctionDescriptor( "substring" ) + .generateSqmExpression( + asList( this, selector, nodeBuilder().literal( 1 ) ), + nodeBuilder().getCharacterType(), + queryEngine + ); + } + else { + throw new UnsupportedOperationException( "Index access is only supported for basic plural and string types, but got: " + sqmPathType ); + } final SqmFunctionPath path = new SqmFunctionPath<>( result ); pathRegistry.register( path ); return path; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmFunctionPath.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmFunctionPath.java index 33b3f237c07e..66879ccc4fa1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmFunctionPath.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmFunctionPath.java @@ -12,6 +12,7 @@ import org.hibernate.metamodel.model.domain.ListPersistentAttribute; import org.hibernate.metamodel.model.domain.MapPersistentAttribute; import org.hibernate.metamodel.model.domain.PluralPersistentAttribute; +import org.hibernate.metamodel.model.domain.internal.BasicSqmPathSource; import org.hibernate.metamodel.model.domain.internal.EmbeddedSqmPathSource; import org.hibernate.query.NotIndexedCollectionException; import org.hibernate.query.hql.spi.SqmCreationState; @@ -31,8 +32,11 @@ import org.hibernate.query.sqm.tree.from.SqmQualifiedJoin; import org.hibernate.spi.NavigablePath; import org.hibernate.type.BasicPluralType; +import org.hibernate.type.BasicType; import jakarta.persistence.metamodel.Bindable; +import jakarta.persistence.metamodel.ManagedType; +import jakarta.persistence.metamodel.Type; import static java.util.Arrays.asList; @@ -56,16 +60,34 @@ public SqmFunctionPath(NavigablePath navigablePath, SqmFunction function) { private static SqmPathSource determinePathSource(NavigablePath navigablePath, SqmFunction function) { //noinspection unchecked final SqmExpressible nodeType = (SqmExpressible) function.getNodeType(); - final EmbeddableDomainType embeddableDomainType = function.nodeBuilder() + final Class bindableJavaType = nodeType.getBindableJavaType(); + final ManagedType managedType = function.nodeBuilder() .getJpaMetamodel() - .embeddable( nodeType.getBindableJavaType() ); - return new EmbeddedSqmPathSource<>( - navigablePath.getFullPath(), - null, - embeddableDomainType, - Bindable.BindableType.SINGULAR_ATTRIBUTE, - false - ); + .findManagedType( bindableJavaType ); + if ( managedType == null ) { + final BasicType basicType = function.nodeBuilder().getTypeConfiguration() + .getBasicTypeForJavaType( bindableJavaType ); + return new BasicSqmPathSource<>( + navigablePath.getFullPath(), + null, + basicType, + basicType.getRelationalJavaType(), + Bindable.BindableType.SINGULAR_ATTRIBUTE, + false + ); + } + else if ( managedType.getPersistenceType() == Type.PersistenceType.EMBEDDABLE ) { + return new EmbeddedSqmPathSource<>( + navigablePath.getFullPath(), + null, + (EmbeddableDomainType) managedType, + Bindable.BindableType.SINGULAR_ATTRIBUTE, + false + ); + } + else { + throw new IllegalArgumentException( "Unsupported return type for function: " + bindableJavaType.getName() ); + } } public SqmFunction getFunction() { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/StringBracketSyntaxTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/StringBracketSyntaxTest.java new file mode 100644 index 000000000000..6f260c3a85e8 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/StringBracketSyntaxTest.java @@ -0,0 +1,62 @@ +package org.hibernate.orm.test.hql; + +import org.hibernate.testing.orm.domain.StandardDomainModel; +import org.hibernate.testing.orm.domain.gambit.BasicEntity; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +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 static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel(standardModels = StandardDomainModel.GAMBIT) +@SessionFactory +@JiraKey("HHH-18089") +public class StringBracketSyntaxTest { + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + BasicEntity entity = new BasicEntity(1, "Hello World"); + session.persist( entity ); + } + ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete from BasicEntity" ).executeUpdate(); + } + ); + } + + @Test + public void testCharAtSyntax(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Character firstChar = session.createQuery( "select e.data[1] from BasicEntity e", Character.class ) + .getSingleResult(); + assertThat( firstChar ).isEqualTo( 'H' ); + } + ); + } + + @Test + public void testSubstringSyntax(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String substring = session.createQuery( "select e.data[1:6] from BasicEntity e", String.class ) + .getSingleResult(); + assertThat( substring ).isEqualTo( "Hello " ); + } + ); + } + + +} diff --git a/release-announcement.adoc b/release-announcement.adoc index 9a2cb551bc0f..60e3af6b09c8 100644 --- a/release-announcement.adoc +++ b/release-announcement.adoc @@ -140,3 +140,12 @@ Plenty of syntax sugar for array operations was added: |Overlaps predicate for overlaps check |=== +[[string-syntax-sugar]] +== Syntax sugar for string functions + +The bracket syntax can now also be used for string typed expressions to select a single character by index, +or obtain a substring by start and end index. + +`stringPath[2]` is syntax sugar for `substring(stringPath, 2, 1)` and returns a `Character`. +`stringPath[2:3]` is syntax sugar for `substring(stringPath, 2, 3-2+1)`, +where `3-2+1` is the expression to determine the desired string length.