HHH-18089 Support bracket syntax with string types

This commit is contained in:
Christian Beikov 2024-05-13 10:24:22 +02:00
parent c8aa4f39da
commit 8b5cdba5bc
7 changed files with 204 additions and 66 deletions

View File

@ -5333,15 +5333,48 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
}
else if ( ctx.simplePath() != null && ctx.slicedPathAccessFragment() != null ) {
final List<HqlParser.ExpressionContext> 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() + "'" );

View File

@ -25,10 +25,14 @@ import org.hibernate.query.sqm.tree.expression.NullSqmExpressible;
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 class StandardFunctionReturnTypeResolvers {
// 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 class StandardFunctionReturnTypeResolvers {
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 @@ public class StandardFunctionReturnTypeResolvers {
//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(

View File

@ -3694,7 +3694,10 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
}
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 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
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 abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
}
@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;
}

View File

@ -110,17 +110,30 @@ public class SqmBasicValuedSimplePath<T>
if ( indexedPath != null ) {
return indexedPath;
}
if ( !( getNodeType().getSqmPathType() instanceof BasicPluralType<?, ?> ) ) {
throw new UnsupportedOperationException( "Index access is only supported for basic plural types." );
}
final DomainType<T> 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<Object> path = new SqmFunctionPath<>( result );
pathRegistry.register( path );
return path;

View File

@ -12,6 +12,7 @@ import org.hibernate.metamodel.model.domain.EntityDomainType;
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.SqmFrom;
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 class SqmFunctionPath<T> extends AbstractSqmPath<T> {
private static <X> SqmPathSource<X> determinePathSource(NavigablePath navigablePath, SqmFunction<?> function) {
//noinspection unchecked
final SqmExpressible<X> nodeType = (SqmExpressible<X>) function.getNodeType();
final EmbeddableDomainType<X> embeddableDomainType = function.nodeBuilder()
final Class<X> bindableJavaType = nodeType.getBindableJavaType();
final ManagedType<X> 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<X> 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<X>) managedType,
Bindable.BindableType.SINGULAR_ATTRIBUTE,
false
);
}
else {
throw new IllegalArgumentException( "Unsupported return type for function: " + bindableJavaType.getName() );
}
}
public SqmFunction<?> getFunction() {

View File

@ -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 " );
}
);
}
}

View File

@ -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.